diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml new file mode 100644 index 0000000000000..40be42461c493 --- /dev/null +++ b/.buildkite/ftr_configs.yml @@ -0,0 +1,239 @@ +disabled: + # TODO: Enable once RBAC timeline search strategy test updated + - x-pack/test/timeline/security_and_spaces/config_basic.ts + + # Base config files, only necessary to inform config finding script + - test/functional/config.base.js + - x-pack/test/functional/config.base.js + - x-pack/test/detection_engine_api_integration/security_and_spaces/config.base.ts + - x-pack/test/functional_enterprise_search/base_config.ts + - test/server_integration/config.base.js + + # QA suites that are run out-of-band + - x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js + - x-pack/test/upgrade/config.ts + - test/functional/config.edge.js + - x-pack/test/functional/config.edge.js + + # Cypress configs, for now these are still run manually + - x-pack/test/fleet_cypress/cli_config.ts + - x-pack/test/fleet_cypress/config.ts + - x-pack/test/fleet_cypress/visual_config.ts + - x-pack/test/functional_enterprise_search/cypress.config.ts + - x-pack/test/osquery_cypress/cli_config.ts + - x-pack/test/osquery_cypress/config.ts + - x-pack/test/osquery_cypress/visual_config.ts + - x-pack/test/security_solution_cypress/cases_cli_config.ts + - x-pack/test/security_solution_cypress/ccs_config.ts + - x-pack/test/security_solution_cypress/cli_config.ts + - x-pack/test/security_solution_cypress/config.firefox.ts + - x-pack/test/security_solution_cypress/config.ts + - x-pack/test/security_solution_cypress/response_ops_cli_config.ts + - x-pack/test/security_solution_cypress/upgrade_config.ts + - x-pack/test/security_solution_cypress/visual_config.ts + - x-pack/test/functional_enterprise_search/with_host_configured.config.ts + - x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts + - x-pack/plugins/apm/ftr_e2e/ftr_config.ts + + # Elastic Synthetics configs + - x-pack/plugins/synthetics/e2e/config.ts + - x-pack/plugins/synthetics/e2e/playwright_run.ts + + # Configs that exist but weren't running in CI when this file was introduced + - test/visual_regression/config.ts + - x-pack/test/visual_regression/config.ts + - x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.ts + - x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/config.ts + - x-pack/test/alerting_api_integration/spaces_only_legacy/config.ts + - x-pack/test/banners_functional/config.ts + - x-pack/test/cloud_integration/config.ts + - x-pack/test/performance/config.playwright.ts + - x-pack/test/load/config.ts + - x-pack/test/plugin_api_perf/config.js + - x-pack/test/screenshot_creation/config.ts + +enabled: + - test/accessibility/config.ts + - test/analytics/config.ts + - test/api_integration/config.js + - test/examples/config.js + - test/functional/apps/bundles/config.ts + - test/functional/apps/console/config.ts + - test/functional/apps/context/config.ts + - test/functional/apps/dashboard_elements/config.ts + - test/functional/apps/dashboard/group1/config.ts + - test/functional/apps/dashboard/group2/config.ts + - test/functional/apps/dashboard/group3/config.ts + - test/functional/apps/dashboard/group4/config.ts + - test/functional/apps/dashboard/group5/config.ts + - test/functional/apps/discover/config.ts + - test/functional/apps/getting_started/config.ts + - test/functional/apps/home/config.ts + - test/functional/apps/management/config.ts + - test/functional/apps/saved_objects_management/config.ts + - test/functional/apps/status_page/config.ts + - test/functional/apps/visualize/group1/config.ts + - test/functional/apps/visualize/group2/config.ts + - test/functional/apps/visualize/group3/config.ts + - test/functional/apps/visualize/group4/config.ts + - test/functional/apps/visualize/group5/config.ts + - test/functional/apps/visualize/group6/config.ts + - test/functional/apps/visualize/replaced_vislib_chart_types/config.ts + - test/functional/config.ccs.ts + - test/functional/config.firefox.js + - test/interactive_setup_api_integration/enrollment_flow.config.ts + - test/interactive_setup_api_integration/manual_configuration_flow_without_tls.config.ts + - test/interactive_setup_api_integration/manual_configuration_flow.config.ts + - test/interactive_setup_functional/enrollment_token.config.ts + - test/interactive_setup_functional/manual_configuration_without_security.config.ts + - test/interactive_setup_functional/manual_configuration_without_tls.config.ts + - test/interactive_setup_functional/manual_configuration.config.ts + - test/interpreter_functional/config.ts + - test/new_visualize_flow/config.ts + - test/plugin_functional/config.ts + - test/server_integration/http/platform/config.status.ts + - test/server_integration/http/platform/config.ts + - test/server_integration/http/ssl_redirect/config.js + - test/server_integration/http/ssl_with_p12_intermediate/config.js + - test/server_integration/http/ssl_with_p12/config.js + - test/server_integration/http/ssl/config.js + - test/ui_capabilities/newsfeed_err/config.ts + - x-pack/test/accessibility/config.ts + - x-pack/test/alerting_api_integration/basic/config.ts + - x-pack/test/alerting_api_integration/security_and_spaces/group1/config.ts + - x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts + - x-pack/test/alerting_api_integration/spaces_only/config.ts + - x-pack/test/api_integration_basic/config.ts + - x-pack/test/api_integration/config_security_basic.ts + - x-pack/test/api_integration/config_security_trial.ts + - x-pack/test/api_integration/config.ts + - x-pack/test/apm_api_integration/basic/config.ts + - x-pack/test/apm_api_integration/rules/config.ts + - x-pack/test/apm_api_integration/trial/config.ts + - x-pack/test/cases_api_integration/security_and_spaces/config_basic.ts + - x-pack/test/cases_api_integration/security_and_spaces/config_trial.ts + - x-pack/test/cases_api_integration/spaces_only/config.ts + - x-pack/test/detection_engine_api_integration/basic/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group1/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group2/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group3/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group4/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group5/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group6/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group7/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group8/config.ts + - x-pack/test/detection_engine_api_integration/security_and_spaces/group9/config.ts + - x-pack/test/encrypted_saved_objects_api_integration/config.ts + - x-pack/test/endpoint_api_integration_no_ingest/config.ts + - x-pack/test/examples/config.ts + - x-pack/test/fleet_api_integration/config.ts + - x-pack/test/fleet_functional/config.ts + - x-pack/test/functional_basic/config.ts + - x-pack/test/functional_cors/config.ts + - x-pack/test/functional_embedded/config.ts + - x-pack/test/functional_enterprise_search/without_host_configured.config.ts + - x-pack/test/functional_execution_context/config.ts + - x-pack/test/functional_synthetics/config.js + - x-pack/test/functional_with_es_ssl/config.ts + - x-pack/test/functional/apps/advanced_settings/config.ts + - x-pack/test/functional/apps/api_keys/config.ts + - x-pack/test/functional/apps/apm/config.ts + - x-pack/test/functional/apps/canvas/config.ts + - x-pack/test/functional/apps/cross_cluster_replication/config.ts + - x-pack/test/functional/apps/dashboard/group1/config.ts + - x-pack/test/functional/apps/dashboard/group2/config.ts + - x-pack/test/functional/apps/data_views/config.ts + - x-pack/test/functional/apps/dev_tools/config.ts + - x-pack/test/functional/apps/discover/config.ts + - x-pack/test/functional/apps/graph/config.ts + - x-pack/test/functional/apps/grok_debugger/config.ts + - x-pack/test/functional/apps/home/config.ts + - x-pack/test/functional/apps/index_lifecycle_management/config.ts + - x-pack/test/functional/apps/index_management/config.ts + - x-pack/test/functional/apps/infra/config.ts + - x-pack/test/functional/apps/ingest_pipelines/config.ts + - x-pack/test/functional/apps/lens/group1/config.ts + - x-pack/test/functional/apps/lens/group2/config.ts + - x-pack/test/functional/apps/lens/group3/config.ts + - x-pack/test/functional/apps/license_management/config.ts + - x-pack/test/functional/apps/logstash/config.ts + - x-pack/test/functional/apps/management/config.ts + - x-pack/test/functional/apps/maps/group1/config.ts + - x-pack/test/functional/apps/maps/group2/config.ts + - x-pack/test/functional/apps/maps/group3/config.ts + - x-pack/test/functional/apps/maps/group4/config.ts + - x-pack/test/functional/apps/ml/data_visualizer/config.ts + - x-pack/test/functional/apps/ml/group1/config.ts + - x-pack/test/functional/apps/ml/group2/config.ts + - x-pack/test/functional/apps/ml/group3/config.ts + - x-pack/test/functional/apps/monitoring/config.ts + - x-pack/test/functional/apps/remote_clusters/config.ts + - x-pack/test/functional/apps/reporting_management/config.ts + - x-pack/test/functional/apps/rollup_job/config.ts + - x-pack/test/functional/apps/saved_objects_management/config.ts + - x-pack/test/functional/apps/security/config.ts + - x-pack/test/functional/apps/snapshot_restore/config.ts + - x-pack/test/functional/apps/spaces/config.ts + - x-pack/test/functional/apps/status_page/config.ts + - x-pack/test/functional/apps/transform/config.ts + - x-pack/test/functional/apps/upgrade_assistant/config.ts + - x-pack/test/functional/apps/uptime/config.ts + - x-pack/test/functional/apps/visualize/config.ts + - x-pack/test/functional/apps/watcher/config.ts + - x-pack/test/functional/config_security_basic.ts + - x-pack/test/functional/config.ccs.ts + - x-pack/test/functional/config.firefox.js + - x-pack/test/licensing_plugin/config.public.ts + - x-pack/test/licensing_plugin/config.ts + - x-pack/test/lists_api_integration/security_and_spaces/config.ts + - x-pack/test/observability_api_integration/basic/config.ts + - x-pack/test/observability_api_integration/trial/config.ts + - x-pack/test/observability_functional/with_rac_write.config.ts + - x-pack/test/plugin_api_integration/config.ts + - x-pack/test/plugin_functional/config.ts + - x-pack/test/reporting_api_integration/reporting_and_security.config.ts + - x-pack/test/reporting_api_integration/reporting_without_security.config.ts + - x-pack/test/reporting_functional/reporting_and_deprecated_security.config.ts + - x-pack/test/reporting_functional/reporting_and_security.config.ts + - x-pack/test/reporting_functional/reporting_without_security.config.ts + - x-pack/test/rule_registry/security_and_spaces/config_basic.ts + - x-pack/test/rule_registry/security_and_spaces/config_trial.ts + - x-pack/test/rule_registry/spaces_only/config_basic.ts + - x-pack/test/rule_registry/spaces_only/config_trial.ts + - x-pack/test/saved_object_api_integration/security_and_spaces/config_basic.ts + - x-pack/test/saved_object_api_integration/security_and_spaces/config_trial.ts + - x-pack/test/saved_object_api_integration/spaces_only/config.ts + - x-pack/test/saved_object_tagging/api_integration/security_and_spaces/config.ts + - x-pack/test/saved_object_tagging/api_integration/tagging_api/config.ts + - x-pack/test/saved_object_tagging/functional/config.ts + - x-pack/test/saved_objects_field_count/config.ts + - x-pack/test/search_sessions_integration/config.ts + - x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts + - x-pack/test/security_api_integration/anonymous.config.ts + - x-pack/test/security_api_integration/audit.config.ts + - x-pack/test/security_api_integration/http_bearer.config.ts + - x-pack/test/security_api_integration/http_no_auth_providers.config.ts + - x-pack/test/security_api_integration/kerberos_anonymous_access.config.ts + - x-pack/test/security_api_integration/kerberos.config.ts + - x-pack/test/security_api_integration/login_selector.config.ts + - x-pack/test/security_api_integration/oidc_implicit_flow.config.ts + - x-pack/test/security_api_integration/oidc.config.ts + - x-pack/test/security_api_integration/pki.config.ts + - x-pack/test/security_api_integration/saml.config.ts + - x-pack/test/security_api_integration/session_idle.config.ts + - x-pack/test/security_api_integration/session_invalidate.config.ts + - x-pack/test/security_api_integration/session_lifespan.config.ts + - x-pack/test/security_api_integration/token.config.ts + - x-pack/test/security_functional/login_selector.config.ts + - x-pack/test/security_functional/oidc.config.ts + - x-pack/test/security_functional/saml.config.ts + - x-pack/test/security_solution_endpoint_api_int/config.ts + - x-pack/test/security_solution_endpoint/config.ts + - x-pack/test/spaces_api_integration/security_and_spaces/config_basic.ts + - x-pack/test/spaces_api_integration/security_and_spaces/config_trial.ts + - x-pack/test/spaces_api_integration/spaces_only/config.ts + - x-pack/test/timeline/security_and_spaces/config_trial.ts + - x-pack/test/ui_capabilities/security_and_spaces/config.ts + - x-pack/test/ui_capabilities/spaces_only/config.ts + - x-pack/test/upgrade_assistant_integration/config.js + - x-pack/test/usage_collection/config.ts diff --git a/.buildkite/package-lock.json b/.buildkite/package-lock.json index cfbcb9ec1aae8..d848470b3ff68 100644 --- a/.buildkite/package-lock.json +++ b/.buildkite/package-lock.json @@ -8,7 +8,7 @@ "name": "kibana-buildkite", "version": "1.0.0", "dependencies": { - "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#0f95579ace8100de9f1ad4cc16976b9ec6d5841e" + "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#b9f6b423059cac7554a7402277f2ad3ecfe132a4" } }, "node_modules/@nodelib/fs.scandir": { @@ -355,8 +355,8 @@ }, "node_modules/kibana-buildkite-library": { "version": "1.0.0", - "resolved": "git+https://git@github.com/elastic/kibana-buildkite-library.git#0f95579ace8100de9f1ad4cc16976b9ec6d5841e", - "integrity": "sha512-Ayiyy3rAE/jOWcR65vxiv4zacMlpxuRZ+WKvly6magfClWTWIUTcW1aiOH2/PYWP3faiCbIDHOyxLeGGajk5dQ==", + "resolved": "git+https://git@github.com/elastic/kibana-buildkite-library.git#b9f6b423059cac7554a7402277f2ad3ecfe132a4", + "integrity": "sha512-HsSPeCrKwJKa+1urq/AzELmA1hsrwZjmOKzWzEYcQ63ZAnn8G3QWrGL0dNZQxIto3243EEs6Ne1pUVTMtEJr+Q==", "license": "MIT", "dependencies": { "@octokit/rest": "^18.10.0", @@ -801,9 +801,9 @@ } }, "kibana-buildkite-library": { - "version": "git+https://git@github.com/elastic/kibana-buildkite-library.git#0f95579ace8100de9f1ad4cc16976b9ec6d5841e", - "integrity": "sha512-Ayiyy3rAE/jOWcR65vxiv4zacMlpxuRZ+WKvly6magfClWTWIUTcW1aiOH2/PYWP3faiCbIDHOyxLeGGajk5dQ==", - "from": "kibana-buildkite-library@git+https://git@github.com/elastic/kibana-buildkite-library#0f95579ace8100de9f1ad4cc16976b9ec6d5841e", + "version": "git+https://git@github.com/elastic/kibana-buildkite-library.git#b9f6b423059cac7554a7402277f2ad3ecfe132a4", + "integrity": "sha512-HsSPeCrKwJKa+1urq/AzELmA1hsrwZjmOKzWzEYcQ63ZAnn8G3QWrGL0dNZQxIto3243EEs6Ne1pUVTMtEJr+Q==", + "from": "kibana-buildkite-library@git+https://git@github.com/elastic/kibana-buildkite-library#b9f6b423059cac7554a7402277f2ad3ecfe132a4", "requires": { "@octokit/rest": "^18.10.0", "axios": "^0.21.4", diff --git a/.buildkite/package.json b/.buildkite/package.json index 551def1fa1800..4e46ba6637027 100644 --- a/.buildkite/package.json +++ b/.buildkite/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "private": true, "dependencies": { - "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#0f95579ace8100de9f1ad4cc16976b9ec6d5841e" + "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#b9f6b423059cac7554a7402277f2ad3ecfe132a4" } } diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml index 729d0d1b37b5f..bdbce0745c1f8 100755 --- a/.buildkite/pipelines/es_snapshots/verify.yml +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -27,60 +27,28 @@ steps: if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''" timeout_in_minutes: 60 - - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh - label: 'Default CI Group' - parallelism: 31 - agents: - queue: n2-4 - depends_on: build - timeout_in_minutes: 150 - key: default-cigroup - retry: - automatic: - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_cigroup.sh - label: 'OSS CI Group' - parallelism: 12 - agents: - queue: ci-group-4d - depends_on: build - timeout_in_minutes: 120 - key: oss-cigroup - retry: - automatic: - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/test/pick_jest_config_run_order.sh - label: 'Pick Jest Config Run Order' + - command: .buildkite/scripts/steps/test/pick_test_group_run_order.sh + label: 'Pick Test Group Run Order' agents: queue: kibana-default env: - FILTER_JEST_CONFIG_TYPE: integration + JEST_UNIT_SCRIPT: '.buildkite/scripts/steps/test/jest.sh' + JEST_INTEGRATION_SCRIPT: '.buildkite/scripts/steps/test/jest_integration.sh' + FTR_CONFIGS_SCRIPT: '.buildkite/scripts/steps/test/ftr_configs.sh' + LIMIT_CONFIG_TYPE: integration,functional retry: automatic: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/test/api_integration.sh - label: 'API Integration Tests' - agents: - queue: n2-2 - timeout_in_minutes: 120 - key: api-integration - - command: .buildkite/scripts/steps/es_snapshots/trigger_promote.sh label: Trigger promotion timeout_in_minutes: 10 agents: queue: kibana-default depends_on: - - default-cigroup - - oss-cigroup + - ftr-configs - jest-integration - - api-integration - wait: ~ continue_on_failure: true diff --git a/.buildkite/pipelines/flaky_tests/groups.json b/.buildkite/pipelines/flaky_tests/groups.json index e471d5c6a8679..78f8e6e56b904 100644 --- a/.buildkite/pipelines/flaky_tests/groups.json +++ b/.buildkite/pipelines/flaky_tests/groups.json @@ -1,46 +1,20 @@ { "groups": [ { - "key": "oss/cigroup", - "name": "OSS CI Group", - "ciGroups": 12 - }, - { - "key": "oss/firefox", - "name": "OSS Firefox" - }, - { - "key": "oss/accessibility", - "name": "OSS Accessibility" - }, - { - "key": "xpack/cypress/security_solution", + "key": "cypress/security_solution", "name": "Security Solution - Cypress" }, { - "key": "xpack/cypress/osquery_cypress", + "key": "cypress/osquery_cypress", "name": "Osquery - Cypress" }, { - "key": "xpack/cypress/fleet_cypress", + "key": "cypress/fleet_cypress", "name": "Fleet - Cypress" }, { - "key": "xpack/cypress/apm_cypress", + "key": "cypress/apm_cypress", "name": "APM - Cypress" - }, - { - "key": "xpack/cigroup", - "name": "Default CI Group", - "ciGroups": 30 - }, - { - "key": "xpack/firefox", - "name": "Default Firefox" - }, - { - "key": "xpack/accessibility", - "name": "Default Accessibility" } ] } diff --git a/.buildkite/pipelines/flaky_tests/pipeline.js b/.buildkite/pipelines/flaky_tests/pipeline.js index b7f93412edb37..d62156a08c55a 100644 --- a/.buildkite/pipelines/flaky_tests/pipeline.js +++ b/.buildkite/pipelines/flaky_tests/pipeline.js @@ -6,56 +6,179 @@ * Side Public License, v 1. */ +const configJson = process.env.KIBANA_FLAKY_TEST_RUNNER_CONFIG; +if (!configJson) { + console.error('+++ Triggering directly is not supported anymore'); + console.error( + `Please use the "Trigger Flaky Test Runner" UI to run the Flaky Test Runner. You can find the UI at the URL below:` + ); + console.error('\n https://ci-stats.kibana.dev/trigger_flaky_test_runner\n'); + process.exit(1); +} + const groups = /** @type {Array<{key: string, name: string, ciGroups: number }>} */ ( require('./groups.json').groups ); -const stepInput = (key, nameOfSuite) => { - return { - key: `ftsr-suite/${key}`, - text: nameOfSuite, - required: false, - default: '0', +const concurrency = process.env.KIBANA_FLAKY_TEST_CONCURRENCY + ? parseInt(process.env.KIBANA_FLAKY_TEST_CONCURRENCY, 10) + : 25; + +if (Number.isNaN(concurrency)) { + throw new Error( + `invalid KIBANA_FLAKY_TEST_CONCURRENCY: ${process.env.KIBANA_FLAKY_TEST_CONCURRENCY}` + ); +} + +const BASE_JOBS = 1; +const MAX_JOBS = 500; + +function getTestSuitesFromJson(json) { + const fail = (errorMsg) => { + console.error('+++ Invalid test config provided'); + console.error(`${errorMsg}: ${json}`); + process.exit(1); }; -}; -const inputs = [ - { - key: 'ftsr-override-count', - text: 'Override for all suites', - default: '0', - required: true, - }, -]; - -for (const group of groups) { - if (!group.ciGroups) { - inputs.push(stepInput(group.key, group.name)); - } else { - for (let i = 1; i <= group.ciGroups; i++) { - inputs.push(stepInput(`${group.key}/${i}`, `${group.name} ${i}`)); + let parsed; + try { + parsed = JSON.parse(json); + } catch (error) { + fail(`JSON test config did not parse correctly`); + } + + if (!Array.isArray(parsed)) { + fail(`JSON test config must be an array`); + } + + /** @type {Array<{ type: 'group', key: string; count: number } | { type: 'ftrConfig', ftrConfig: string; count: number }>} */ + const testSuites = []; + for (const item of parsed) { + if (typeof item !== 'object' || item === null) { + fail(`testSuites must be objects`); + } + + const count = item.count; + if (typeof count !== 'number') { + fail(`testSuite.count must be a number`); + } + + const type = item.type; + if (type !== 'ftrConfig' && type !== 'group') { + fail(`testSuite.type must be either "ftrConfig" or "group"`); } + + if (item.type === 'ftrConfig') { + const ftrConfig = item.ftrConfig; + if (typeof ftrConfig !== 'string') { + fail(`testSuite.ftrConfig must be a string`); + } + + testSuites.push({ + ftrConfig, + count, + }); + continue; + } + + const key = item.key; + if (typeof key !== 'string') { + fail(`testSuite.key must be a string`); + } + testSuites.push({ + key, + count, + }); } + + return testSuites; +} + +const testSuites = getTestSuitesFromJson(configJson); + +const totalJobs = testSuites.reduce((acc, t) => acc + t.count, BASE_JOBS); + +if (totalJobs > MAX_JOBS) { + console.error('+++ Too many tests'); + console.error( + `Buildkite builds can only contain ${MAX_JOBS} jobs in total. Found ${totalJobs} based on this config. Make sure your test runs are less than ${ + MAX_JOBS - BASE_JOBS + }` + ); + process.exit(1); } +const steps = []; const pipeline = { - steps: [ - { - input: 'Number of Runs - Click Me', - fields: inputs, - if: `build.env('KIBANA_FLAKY_TEST_RUNNER_CONFIG') == null`, - }, - { - wait: '~', - }, - { - command: '.buildkite/pipelines/flaky_tests/runner.sh', - label: 'Create pipeline', + env: { + IGNORE_SHIP_CI_STATS_ERROR: 'true', + }, + steps, +}; + +steps.push({ + command: '.buildkite/scripts/steps/build_kibana.sh', + label: 'Build Kibana Distribution and Plugins', + agents: { queue: 'c2-8' }, + key: 'build', + if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''", +}); + +for (const testSuite of testSuites) { + if (testSuite.count <= 0) { + continue; + } + + if (testSuite.ftrConfig) { + steps.push({ + command: `.buildkite/scripts/steps/test/ftr_configs.sh`, + env: { + FTR_CONFIG: testSuite.ftrConfig, + }, + label: `FTR Config: ${testSuite.ftrConfig}`, + parallelism: testSuite.count, + concurrency: concurrency, + concurrency_group: process.env.UUID, + concurrency_method: 'eager', agents: { - queue: 'kibana-default', + queue: 'n2-4-spot-2', }, - }, - ], -}; + depends_on: 'build', + timeout_in_minutes: 150, + retry: { + automatic: [ + { exit_status: '-1', limit: 3 }, + // { exit_status: '*', limit: 1 }, + ], + }, + }); + continue; + } + + const keyParts = testSuite.key.split('/'); + switch (keyParts[0]) { + case 'cypress': + const CYPRESS_SUITE = keyParts[1]; + const group = groups.find((group) => group.key.includes(CYPRESS_SUITE)); + if (!group) { + throw new Error( + `Group configuration was not found in groups.json for the following cypress suite: {${CYPRESS_SUITE}}.` + ); + } + steps.push({ + command: `.buildkite/scripts/steps/functional/${CYPRESS_SUITE}.sh`, + label: group.name, + agents: { queue: 'ci-group-6' }, + depends_on: 'build', + parallelism: testSuite.count, + concurrency: concurrency, + concurrency_group: process.env.UUID, + concurrency_method: 'eager', + }); + break; + default: + throw new Error(`unknown test suite: ${testSuite.key}`); + } +} console.log(JSON.stringify(pipeline, null, 2)); diff --git a/.buildkite/pipelines/flaky_tests/pipeline.sh b/.buildkite/pipelines/flaky_tests/pipeline.sh index 6335cd5490af0..17fa0152e8b34 100755 --- a/.buildkite/pipelines/flaky_tests/pipeline.sh +++ b/.buildkite/pipelines/flaky_tests/pipeline.sh @@ -2,4 +2,7 @@ set -euo pipefail +UUID="$(cat /proc/sys/kernel/random/uuid)" +export UUID + node .buildkite/pipelines/flaky_tests/pipeline.js | buildkite-agent pipeline upload diff --git a/.buildkite/pipelines/flaky_tests/runner.js b/.buildkite/pipelines/flaky_tests/runner.js deleted file mode 100644 index a5cc237a1814a..0000000000000 --- a/.buildkite/pipelines/flaky_tests/runner.js +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const { execSync } = require('child_process'); -const groups = /** @type {Array<{key: string, name: string, ciGroups: number }>} */ ( - require('./groups.json').groups -); - -const concurrency = 25; -const defaultCount = concurrency * 2; -const initialJobs = 3; - -function getTestSuitesFromMetadata() { - const keys = execSync('buildkite-agent meta-data keys') - .toString() - .split('\n') - .filter((k) => k.startsWith('ftsr-suite/')); - - const overrideCount = execSync(`buildkite-agent meta-data get 'ftsr-override-count'`) - .toString() - .trim(); - - const testSuites = []; - for (const key of keys) { - if (!key) { - continue; - } - - const value = - overrideCount && overrideCount !== '0' - ? overrideCount - : execSync(`buildkite-agent meta-data get '${key}'`).toString().trim(); - - const count = value === '' ? defaultCount : parseInt(value); - testSuites.push({ - key: key.replace('ftsr-suite/', ''), - count: count, - }); - } - - return testSuites; -} - -function getTestSuitesFromJson(json) { - const fail = (errorMsg) => { - console.error('+++ Invalid test config provided'); - console.error(`${errorMsg}: ${json}`); - process.exit(1); - }; - - let parsed; - try { - parsed = JSON.parse(json); - } catch (error) { - fail(`JSON test config did not parse correctly`); - } - - if (!Array.isArray(parsed)) { - fail(`JSON test config must be an array`); - } - - /** @type {Array<{ key: string, count: number }>} */ - const testSuites = []; - for (const item of parsed) { - if (typeof item !== 'object' || item === null) { - fail(`testSuites must be objects`); - } - const key = item.key; - if (typeof key !== 'string') { - fail(`testSuite.key must be a string`); - } - const count = item.count; - if (typeof count !== 'number') { - fail(`testSuite.count must be a number`); - } - testSuites.push({ - key, - count, - }); - } - - return testSuites; -} - -const testSuites = process.env.KIBANA_FLAKY_TEST_RUNNER_CONFIG - ? getTestSuitesFromJson(process.env.KIBANA_FLAKY_TEST_RUNNER_CONFIG) - : getTestSuitesFromMetadata(); - -const totalJobs = testSuites.reduce((acc, t) => acc + t.count, initialJobs); - -if (totalJobs > 500) { - console.error('+++ Too many tests'); - console.error( - `Buildkite builds can only contain 500 steps in total. Found ${totalJobs} in total. Make sure your test runs are less than ${ - 500 - initialJobs - }` - ); - process.exit(1); -} - -const steps = []; -const pipeline = { - env: { - IGNORE_SHIP_CI_STATS_ERROR: 'true', - }, - steps: steps, -}; - -steps.push({ - command: '.buildkite/scripts/steps/build_kibana.sh', - label: 'Build Kibana Distribution and Plugins', - agents: { queue: 'c2-8' }, - key: 'build', - if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''", -}); - -for (const testSuite of testSuites) { - const TEST_SUITE = testSuite.key; - const RUN_COUNT = testSuite.count; - const UUID = process.env.UUID; - - const JOB_PARTS = TEST_SUITE.split('/'); - const IS_XPACK = JOB_PARTS[0] === 'xpack'; - const TASK = JOB_PARTS[1]; - const CI_GROUP = JOB_PARTS.length > 2 ? JOB_PARTS[2] : ''; - - if (RUN_COUNT < 1) { - continue; - } - - switch (TASK) { - case 'cigroup': - if (IS_XPACK) { - steps.push({ - command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/xpack_cigroup.sh`, - label: `Default CI Group ${CI_GROUP}`, - agents: { queue: 'n2-4' }, - depends_on: 'build', - parallelism: RUN_COUNT, - concurrency: concurrency, - concurrency_group: UUID, - concurrency_method: 'eager', - }); - } else { - steps.push({ - command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/oss_cigroup.sh`, - label: `OSS CI Group ${CI_GROUP}`, - agents: { queue: 'ci-group-4d' }, - depends_on: 'build', - parallelism: RUN_COUNT, - concurrency: concurrency, - concurrency_group: UUID, - concurrency_method: 'eager', - }); - } - break; - - case 'firefox': - steps.push({ - command: `.buildkite/scripts/steps/functional/${IS_XPACK ? 'xpack' : 'oss'}_firefox.sh`, - label: `${IS_XPACK ? 'Default' : 'OSS'} Firefox`, - agents: { queue: IS_XPACK ? 'n2-4' : 'ci-group-4d' }, - depends_on: 'build', - parallelism: RUN_COUNT, - concurrency: concurrency, - concurrency_group: UUID, - concurrency_method: 'eager', - }); - break; - - case 'accessibility': - steps.push({ - command: `.buildkite/scripts/steps/functional/${ - IS_XPACK ? 'xpack' : 'oss' - }_accessibility.sh`, - label: `${IS_XPACK ? 'Default' : 'OSS'} Accessibility`, - agents: { queue: IS_XPACK ? 'n2-4' : 'ci-group-4d' }, - depends_on: 'build', - parallelism: RUN_COUNT, - concurrency: concurrency, - concurrency_group: UUID, - concurrency_method: 'eager', - }); - break; - - case 'cypress': - const CYPRESS_SUITE = CI_GROUP; - const group = groups.find((group) => group.key.includes(CYPRESS_SUITE)); - if (!group) { - throw new Error( - `Group configuration was not found in groups.json for the following cypress suite: {${CYPRESS_SUITE}}.` - ); - } - steps.push({ - command: `.buildkite/scripts/steps/functional/${CYPRESS_SUITE}.sh`, - label: group.name, - agents: { queue: 'ci-group-6' }, - depends_on: 'build', - parallelism: RUN_COUNT, - concurrency: concurrency, - concurrency_group: UUID, - concurrency_method: 'eager', - }); - break; - } -} - -console.log(JSON.stringify(pipeline, null, 2)); diff --git a/.buildkite/pipelines/flaky_tests/runner.sh b/.buildkite/pipelines/flaky_tests/runner.sh deleted file mode 100755 index b541af88a408a..0000000000000 --- a/.buildkite/pipelines/flaky_tests/runner.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -UUID="$(cat /proc/sys/kernel/random/uuid)" -export UUID - -node .buildkite/pipelines/flaky_tests/runner.js | buildkite-agent pipeline upload diff --git a/.buildkite/pipelines/on_merge.yml b/.buildkite/pipelines/on_merge.yml index 1c89c4e90893b..586199f082925 100644 --- a/.buildkite/pipelines/on_merge.yml +++ b/.buildkite/pipelines/on_merge.yml @@ -49,134 +49,19 @@ steps: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh - label: 'Default CI Group' - parallelism: 31 - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 250 - key: default-cigroup - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_cigroup.sh - label: 'OSS CI Group' - parallelism: 12 - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - key: oss-cigroup - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_accessibility.sh - label: 'OSS Accessibility Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh - label: 'Default Accessibility Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_firefox.sh - label: 'OSS Firefox Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/xpack_firefox.sh - label: 'Default Firefox Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_misc.sh - label: 'OSS Misc Functional Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh - label: 'Saved Object Field Metrics' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/test/pick_jest_config_run_order.sh - label: 'Pick Jest Config Run Order' + - command: .buildkite/scripts/steps/test/pick_test_group_run_order.sh + label: 'Pick Test Group Run Order' agents: queue: kibana-default + env: + JEST_UNIT_SCRIPT: '.buildkite/scripts/steps/test/jest.sh' + JEST_INTEGRATION_SCRIPT: '.buildkite/scripts/steps/test/jest_integration.sh' + FTR_CONFIGS_SCRIPT: '.buildkite/scripts/steps/test/ftr_configs.sh' retry: automatic: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/test/api_integration.sh - label: 'API Integration Tests' - agents: - queue: n2-2-spot - timeout_in_minutes: 120 - key: api-integration - retry: - automatic: - - exit_status: '-1' - limit: 3 - - command: .buildkite/scripts/steps/lint.sh label: 'Linting' agents: @@ -197,10 +82,19 @@ steps: - command: .buildkite/scripts/steps/checks.sh label: 'Checks' + agents: + queue: n2-2-spot + timeout_in_minutes: 60 + retry: + automatic: + - exit_status: '-1' + limit: 3 + + - command: .buildkite/scripts/steps/check_types.sh + label: 'Check Types' agents: queue: c2-8 - key: checks - timeout_in_minutes: 120 + timeout_in_minutes: 60 - command: .buildkite/scripts/steps/storybooks/build_and_upload.sh label: 'Build Storybooks' diff --git a/.buildkite/pipelines/performance/daily.yml b/.buildkite/pipelines/performance/daily.yml index 658ab3a72f186..07e73c27508a6 100644 --- a/.buildkite/pipelines/performance/daily.yml +++ b/.buildkite/pipelines/performance/daily.yml @@ -17,6 +17,13 @@ steps: agents: queue: kb-static-ubuntu depends_on: build + key: tests + + - label: ':shipit: Performance Tests dataset extraction for scalability benchmarking' + command: .buildkite/scripts/steps/functional/scalability_dataset_extraction.sh + agents: + queue: n2-2 + depends_on: tests - wait: ~ continue_on_failure: true diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 4b4104f18c627..73c03e0382a0f 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -15,134 +15,19 @@ steps: if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''" timeout_in_minutes: 60 - - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh - label: 'Default CI Group' - parallelism: 31 - agents: - queue: n2-4-spot-2 - depends_on: build - timeout_in_minutes: 150 - key: default-cigroup - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_cigroup.sh - label: 'OSS CI Group' - parallelism: 12 - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - key: oss-cigroup - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_accessibility.sh - label: 'OSS Accessibility Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh - label: 'Default Accessibility Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_firefox.sh - label: 'OSS Firefox Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/xpack_firefox.sh - label: 'Default Firefox Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/oss_misc.sh - label: 'OSS Misc Functional Tests' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh - label: 'Saved Object Field Metrics' - agents: - queue: n2-4-spot - depends_on: build - timeout_in_minutes: 120 - retry: - automatic: - - exit_status: '-1' - limit: 3 - - exit_status: '*' - limit: 1 - - - command: .buildkite/scripts/steps/test/pick_jest_config_run_order.sh - label: 'Pick Jest Config Run Order' + - command: .buildkite/scripts/steps/test/pick_test_group_run_order.sh + label: 'Pick Test Group Run Order' agents: queue: kibana-default + env: + JEST_UNIT_SCRIPT: '.buildkite/scripts/steps/test/jest.sh' + JEST_INTEGRATION_SCRIPT: '.buildkite/scripts/steps/test/jest_integration.sh' + FTR_CONFIGS_SCRIPT: '.buildkite/scripts/steps/test/ftr_configs.sh' retry: automatic: - exit_status: '*' limit: 1 - - command: .buildkite/scripts/steps/test/api_integration.sh - label: 'API Integration Tests' - agents: - queue: n2-2-spot - timeout_in_minutes: 120 - key: api-integration - retry: - automatic: - - exit_status: '-1' - limit: 3 - - command: .buildkite/scripts/steps/lint.sh label: 'Linting' agents: @@ -159,10 +44,19 @@ steps: - command: .buildkite/scripts/steps/checks.sh label: 'Checks' + agents: + queue: n2-2-spot + timeout_in_minutes: 60 + retry: + automatic: + - exit_status: '-1' + limit: 3 + + - command: .buildkite/scripts/steps/check_types.sh + label: 'Check Types' agents: queue: c2-8 - key: checks - timeout_in_minutes: 120 + timeout_in_minutes: 60 - command: .buildkite/scripts/steps/storybooks/build_and_upload.sh label: 'Build Storybooks' diff --git a/.buildkite/scripts/bootstrap.sh b/.buildkite/scripts/bootstrap.sh index 34461ca6db194..b4ec748b863e2 100755 --- a/.buildkite/scripts/bootstrap.sh +++ b/.buildkite/scripts/bootstrap.sh @@ -6,6 +6,15 @@ source .buildkite/scripts/common/util.sh source .buildkite/scripts/common/setup_bazel.sh echo "--- yarn install and bootstrap" + +# Use the node_modules that is baked into the agent image, if it exists, as a cache +# But only for agents not mounting the workspace on a local ssd or in memory +# It actually ends up being slower to move all of the tiny files between the disks vs extracting archives from the yarn cache +if [[ -d ~/.kibana/node_modules && "$(pwd)" != *"/local-ssd/"* && "$(pwd)" != "/dev/shm"* ]]; then + echo "Using ~/.kibana/node_modules as a starting point" + mv ~/.kibana/node_modules ./ +fi + if ! yarn kbn bootstrap; then echo "bootstrap failed, trying again in 15 seconds" sleep 15 diff --git a/.buildkite/scripts/common/env.sh b/.buildkite/scripts/common/env.sh index 5d2ccda66285b..82c42af67f226 100755 --- a/.buildkite/scripts/common/env.sh +++ b/.buildkite/scripts/common/env.sh @@ -98,3 +98,11 @@ fi export BUILD_TS_REFS_DISABLE=true export DISABLE_BOOTSTRAP_VALIDATION=true + +# Prevent Browserlist from logging on CI about outdated database versions +export BROWSERSLIST_IGNORE_OLD_DATA=true + +# keys used to associate test group data in ci-stats with Jest execution order +export TEST_GROUP_TYPE_UNIT="Jest Unit Tests" +export TEST_GROUP_TYPE_INTEGRATION="Jest Integration Tests" +export TEST_GROUP_TYPE_FUNCTIONAL="Functional Tests" diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.js b/.buildkite/scripts/pipelines/pull_request/pipeline.js index 35ea19c78cd93..6a4610284e400 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.js +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.js @@ -9,19 +9,7 @@ const execSync = require('child_process').execSync; const fs = require('fs'); const { areChangesSkippable, doAnyChangesMatch } = require('kibana-buildkite-library'); - -const SKIPPABLE_PATHS = [ - /^docs\//, - /^rfcs\//, - /^.ci\/.+\.yml$/, - /^.ci\/es-snapshots\//, - /^.ci\/pipeline-library\//, - /^.ci\/Jenkinsfile_[^\/]+$/, - /^\.github\//, - /\.md$/, - /^\.backportrc\.json$/, - /^nav-kibana-dev\.docnav\.json$/, -]; +const { SKIPPABLE_PR_MATCHERS } = require('./skippable_pr_matchers'); const REQUIRED_PATHS = [ // this file is auto-generated and changes to it need to be validated with CI @@ -47,7 +35,7 @@ const uploadPipeline = (pipelineContent) => { (async () => { try { - const skippable = await areChangesSkippable(SKIPPABLE_PATHS, REQUIRED_PATHS); + const skippable = await areChangesSkippable(SKIPPABLE_PR_MATCHERS, REQUIRED_PATHS); if (skippable) { console.log('All changes in PR are skippable. Skipping CI.'); diff --git a/.buildkite/scripts/pipelines/pull_request/skippable_pr_matchers.js b/.buildkite/scripts/pipelines/pull_request/skippable_pr_matchers.js new file mode 100644 index 0000000000000..2a36e66e11cd6 --- /dev/null +++ b/.buildkite/scripts/pipelines/pull_request/skippable_pr_matchers.js @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + SKIPPABLE_PR_MATCHERS: [ + /^docs\//, + /^rfcs\//, + /^.ci\/.+\.yml$/, + /^.ci\/es-snapshots\//, + /^.ci\/pipeline-library\//, + /^.ci\/Jenkinsfile_[^\/]+$/, + /^\.github\//, + /\.md$/, + /^\.backportrc\.json$/, + /^nav-kibana-dev\.docnav\.json$/, + /^src\/dev\/prs\/kibana_qa_pr_list\.json$/, + /^\.buildkite\/scripts\/pipelines\/pull_request\/skippable_pr_matchers\.js$/, + ], +}; diff --git a/.buildkite/scripts/saved_object_field_metrics.sh b/.buildkite/scripts/saved_object_field_metrics.sh index 3b6c63eeff3b0..4cc249db20edc 100755 --- a/.buildkite/scripts/saved_object_field_metrics.sh +++ b/.buildkite/scripts/saved_object_field_metrics.sh @@ -5,9 +5,8 @@ set -euo pipefail source .buildkite/scripts/common/util.sh echo '--- Default Saved Object Field Metrics' -cd "$XPACK_DIR" checks-reporter-with-killswitch "Capture Kibana Saved Objects field count metrics" \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/saved_objects_field_count/config.ts + --config x-pack/test/saved_objects_field_count/config.ts diff --git a/.buildkite/scripts/steps/checks/check_types.sh b/.buildkite/scripts/steps/check_types.sh similarity index 84% rename from .buildkite/scripts/steps/checks/check_types.sh rename to .buildkite/scripts/steps/check_types.sh index 3b649a73e8060..eb7a41d74e8d8 100755 --- a/.buildkite/scripts/steps/checks/check_types.sh +++ b/.buildkite/scripts/steps/check_types.sh @@ -4,6 +4,8 @@ set -euo pipefail source .buildkite/scripts/common/util.sh +.buildkite/scripts/bootstrap.sh + echo --- Check Types checks-reporter-with-killswitch "Check Types" \ node scripts/type_check diff --git a/.buildkite/scripts/steps/checks.sh b/.buildkite/scripts/steps/checks.sh index 42de0f2cf2393..024037a8a4bb9 100755 --- a/.buildkite/scripts/steps/checks.sh +++ b/.buildkite/scripts/steps/checks.sh @@ -8,13 +8,11 @@ export DISABLE_BOOTSTRAP_VALIDATION=false .buildkite/scripts/steps/checks/commit/commit.sh .buildkite/scripts/steps/checks/bazel_packages.sh .buildkite/scripts/steps/checks/telemetry.sh -.buildkite/scripts/steps/checks/validate_ci_groups.sh .buildkite/scripts/steps/checks/ts_projects.sh .buildkite/scripts/steps/checks/jest_configs.sh .buildkite/scripts/steps/checks/doc_api_changes.sh .buildkite/scripts/steps/checks/kbn_pm_dist.sh .buildkite/scripts/steps/checks/plugin_list_docs.sh -.buildkite/scripts/steps/checks/check_types.sh .buildkite/scripts/steps/checks/bundle_limits.sh .buildkite/scripts/steps/checks/i18n.sh .buildkite/scripts/steps/checks/file_casing.sh diff --git a/.buildkite/scripts/steps/checks/validate_ci_groups.sh b/.buildkite/scripts/steps/checks/validate_ci_groups.sh deleted file mode 100755 index 1216ff0a55e10..0000000000000 --- a/.buildkite/scripts/steps/checks/validate_ci_groups.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/common/util.sh - -echo --- Ensure that all tests are in a CI Group -checks-reporter-with-killswitch "Ensure that all tests are in a CI Group" \ - node scripts/ensure_all_tests_in_ci_group diff --git a/.buildkite/scripts/steps/functional/fleet_cypress.sh b/.buildkite/scripts/steps/functional/fleet_cypress.sh index 3847ffda08822..7207a2adf454a 100755 --- a/.buildkite/scripts/steps/functional/fleet_cypress.sh +++ b/.buildkite/scripts/steps/functional/fleet_cypress.sh @@ -11,10 +11,8 @@ export JOB=kibana-fleet-cypress echo "--- Fleet Cypress tests" -cd "$XPACK_DIR" - checks-reporter-with-killswitch "Fleet Cypress Tests" \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/fleet_cypress/cli_config.ts + --config x-pack/test/fleet_cypress/cli_config.ts diff --git a/.buildkite/scripts/steps/functional/osquery_cypress.sh b/.buildkite/scripts/steps/functional/osquery_cypress.sh index a0d98713aa379..02766e0530bfb 100755 --- a/.buildkite/scripts/steps/functional/osquery_cypress.sh +++ b/.buildkite/scripts/steps/functional/osquery_cypress.sh @@ -12,10 +12,8 @@ export JOB=kibana-osquery-cypress echo "--- Osquery Cypress tests" -cd "$XPACK_DIR" - checks-reporter-with-killswitch "Osquery Cypress Tests" \ node scripts/functional_tests \ --debug --bail \ - --config test/osquery_cypress/cli_config.ts + --config x-pack/test/osquery_cypress/cli_config.ts diff --git a/.buildkite/scripts/steps/functional/oss_accessibility.sh b/.buildkite/scripts/steps/functional/oss_accessibility.sh deleted file mode 100755 index e8c65cfa760b2..0000000000000 --- a/.buildkite/scripts/steps/functional/oss_accessibility.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -echo --- OSS Accessibility Tests - -checks-reporter-with-killswitch "Kibana accessibility tests" \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/accessibility/config.ts diff --git a/.buildkite/scripts/steps/functional/oss_cigroup.sh b/.buildkite/scripts/steps/functional/oss_cigroup.sh deleted file mode 100755 index a7a5960a41afe..0000000000000 --- a/.buildkite/scripts/steps/functional/oss_cigroup.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -export CI_GROUP=${CI_GROUP:-$((BUILDKITE_PARALLEL_JOB+1))} -export JOB=kibana-oss-ciGroup${CI_GROUP} - -echo "--- OSS CI Group $CI_GROUP" - -checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" \ - node scripts/functional_tests \ - --bail \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --include-tag "ciGroup$CI_GROUP" diff --git a/.buildkite/scripts/steps/functional/oss_firefox.sh b/.buildkite/scripts/steps/functional/oss_firefox.sh deleted file mode 100755 index e953973da62d6..0000000000000 --- a/.buildkite/scripts/steps/functional/oss_firefox.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -echo --- OSS Firefox Smoke Tests - -checks-reporter-with-killswitch "Firefox smoke test" \ - node scripts/functional_tests \ - --bail --debug \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --include-tag "includeFirefox" \ - --config test/functional/config.firefox.js diff --git a/.buildkite/scripts/steps/functional/oss_misc.sh b/.buildkite/scripts/steps/functional/oss_misc.sh deleted file mode 100755 index 22d4eda608cc2..0000000000000 --- a/.buildkite/scripts/steps/functional/oss_misc.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -# Required, at least for plugin_functional tests -.buildkite/scripts/build_kibana_plugins.sh - -echo --- Plugin Functional Tests -checks-reporter-with-killswitch "Plugin Functional Tests" \ - node scripts/functional_tests \ - --config test/plugin_functional/config.ts \ - --bail \ - --debug - -echo --- Interpreter Functional Tests -checks-reporter-with-killswitch "Interpreter Functional Tests" \ - node scripts/functional_tests \ - --config test/interpreter_functional/config.ts \ - --bail \ - --debug \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" - -echo --- Server Integration Tests -checks-reporter-with-killswitch "Server Integration Tests" \ - node scripts/functional_tests \ - --config test/server_integration/http/ssl/config.js \ - --config test/server_integration/http/ssl_redirect/config.js \ - --config test/server_integration/http/platform/config.ts \ - --config test/server_integration/http/ssl_with_p12/config.js \ - --config test/server_integration/http/ssl_with_p12_intermediate/config.js \ - --bail \ - --debug \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" - -# Tests that must be run against source in order to build test plugins -echo --- Status Integration Tests -checks-reporter-with-killswitch "Status Integration Tests" \ - node scripts/functional_tests \ - --config test/server_integration/http/platform/config.status.ts \ - --bail \ - --debug - -# Tests that must be run against source in order to build test plugins -echo --- Analytics Integration Tests -checks-reporter-with-killswitch "Analytics Integration Tests" \ - node scripts/functional_tests \ - --config test/analytics/config.ts \ - --bail \ - --debug diff --git a/.buildkite/scripts/steps/functional/performance_playwright.sh b/.buildkite/scripts/steps/functional/performance_playwright.sh index dad75c9f66a98..a1c3f23ced51e 100644 --- a/.buildkite/scripts/steps/functional/performance_playwright.sh +++ b/.buildkite/scripts/steps/functional/performance_playwright.sh @@ -18,8 +18,6 @@ export TEST_ES_DISABLE_STARTUP=true sleep 120 -cd "$XPACK_DIR" - journeys=("login" "ecommerce_dashboard" "flight_dashboard" "web_logs_dashboard" "promotion_tracking_dashboard" "many_fields_discover") for i in "${journeys[@]}"; do @@ -31,8 +29,8 @@ for i in "${journeys[@]}"; do checks-reporter-with-killswitch "Run Performance Tests with Playwright Config (Journey:${i},Phase: WARMUP)" \ node scripts/functional_tests \ - --config test/performance/config.playwright.ts \ - --include "test/performance/tests/playwright/${i}.ts" \ + --config x-pack/test/performance/config.playwright.ts \ + --include "x-pack/test/performance/tests/playwright/${i}.ts" \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ --debug \ --bail @@ -42,8 +40,8 @@ for i in "${journeys[@]}"; do checks-reporter-with-killswitch "Run Performance Tests with Playwright Config (Journey:${i},Phase: TEST)" \ node scripts/functional_tests \ - --config test/performance/config.playwright.ts \ - --include "test/performance/tests/playwright/${i}.ts" \ + --config x-pack/test/performance/config.playwright.ts \ + --include "x-pack/test/performance/tests/playwright/${i}.ts" \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ --debug \ --bail diff --git a/.buildkite/scripts/steps/functional/response_ops.sh b/.buildkite/scripts/steps/functional/response_ops.sh index d8484854ed29e..9828884e6d6a2 100755 --- a/.buildkite/scripts/steps/functional/response_ops.sh +++ b/.buildkite/scripts/steps/functional/response_ops.sh @@ -8,10 +8,8 @@ export JOB=kibana-security-solution-chrome echo "--- Response Ops Cypress Tests on Security Solution" -cd "$XPACK_DIR" - checks-reporter-with-killswitch "Response Ops Cypress Tests on Security Solution" \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/security_solution_cypress/response_ops_cli_config.ts + --config x-pack/test/security_solution_cypress/response_ops_cli_config.ts diff --git a/.buildkite/scripts/steps/functional/response_ops_cases.sh b/.buildkite/scripts/steps/functional/response_ops_cases.sh index 13d0ef52130a3..2485e025833ed 100755 --- a/.buildkite/scripts/steps/functional/response_ops_cases.sh +++ b/.buildkite/scripts/steps/functional/response_ops_cases.sh @@ -8,10 +8,8 @@ export JOB=kibana-security-solution-chrome echo "--- Response Ops Cases Cypress Tests on Security Solution" -cd "$XPACK_DIR" - checks-reporter-with-killswitch "Response Ops Cases Cypress Tests on Security Solution" \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/security_solution_cypress/cases_cli_config.ts + --config x-pack/test/security_solution_cypress/cases_cli_config.ts diff --git a/.buildkite/scripts/steps/functional/scalability_dataset_extraction.sh b/.buildkite/scripts/steps/functional/scalability_dataset_extraction.sh new file mode 100755 index 0000000000000..1a8bd77bd2893 --- /dev/null +++ b/.buildkite/scripts/steps/functional/scalability_dataset_extraction.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh + +USER_FROM_VAULT="$(retry 5 5 vault read -field=username secret/kibana-issues/dev/apm_parser_performance)" +PASS_FROM_VAULT="$(retry 5 5 vault read -field=password secret/kibana-issues/dev/apm_parser_performance)" +ES_SERVER_URL="https://kibana-ops-e2e-perf.es.us-central1.gcp.cloud.es.io:9243" +BUILD_ID=${BUILDKITE_BUILD_ID} +GCS_BUCKET="gs://kibana-performance/scalability-tests" + +.buildkite/scripts/bootstrap.sh + +echo "--- Extract APM metrics" +journeys=("login" "ecommerce_dashboard" "flight_dashboard" "web_logs_dashboard" "promotion_tracking_dashboard" "many_fields_discover") + +for i in "${journeys[@]}"; do + JOURNEY_NAME="${i}" + echo "Looking for JOURNEY=${JOURNEY_NAME} and BUILD_ID=${BUILD_ID} in APM traces" + + node scripts/extract_performance_testing_dataset \ + --journeyName "${JOURNEY_NAME}" \ + --buildId "${BUILD_ID}" \ + --es-url "${ES_SERVER_URL}" \ + --es-username "${USER_FROM_VAULT}" \ + --es-password "${PASS_FROM_VAULT}" +done + +echo "--- Upload Kibana build, plugins and scalability traces to the public bucket" +mkdir "${BUILD_ID}" +# Archive json files with traces and upload as build artifacts +tar -czf "${BUILD_ID}/scalability_traces.tar.gz" -C target scalability_traces +buildkite-agent artifact upload "${BUILD_ID}/scalability_traces.tar.gz" +# Upload Kibana build, plugins, commit sha and traces to the bucket +buildkite-agent artifact download kibana-default.tar.gz ./"${BUILD_ID}" +buildkite-agent artifact download kibana-default-plugins.tar.gz ./"${BUILD_ID}" +echo "${BUILDKITE_COMMIT}" > "${BUILD_ID}/KIBANA_COMMIT_HASH" +gsutil -m cp -r "${BUILD_ID}" "${GCS_BUCKET}" +echo "--- Update reference to the latest CI build" +echo "${BUILD_ID}" > LATEST +gsutil cp LATEST "${GCS_BUCKET}" \ No newline at end of file diff --git a/.buildkite/scripts/steps/functional/security_solution.sh b/.buildkite/scripts/steps/functional/security_solution.sh index 9b2bfc7207a95..ae81eaa4f48e2 100755 --- a/.buildkite/scripts/steps/functional/security_solution.sh +++ b/.buildkite/scripts/steps/functional/security_solution.sh @@ -8,10 +8,8 @@ export JOB=kibana-security-solution-chrome echo "--- Security Solution tests (Chrome)" -cd "$XPACK_DIR" - checks-reporter-with-killswitch "Security Solution Cypress Tests (Chrome)" \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/security_solution_cypress/cli_config.ts + --config x-pack/test/security_solution_cypress/cli_config.ts diff --git a/.buildkite/scripts/steps/functional/xpack_accessibility.sh b/.buildkite/scripts/steps/functional/xpack_accessibility.sh deleted file mode 100755 index 5b098da858c96..0000000000000 --- a/.buildkite/scripts/steps/functional/xpack_accessibility.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -cd "$XPACK_DIR" - -echo --- Default Accessibility Tests - -checks-reporter-with-killswitch "X-Pack accessibility tests" \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/accessibility/config.ts diff --git a/.buildkite/scripts/steps/functional/xpack_cigroup.sh b/.buildkite/scripts/steps/functional/xpack_cigroup.sh deleted file mode 100755 index 6877e88cff8b3..0000000000000 --- a/.buildkite/scripts/steps/functional/xpack_cigroup.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -export CI_GROUP=${CI_GROUP:-$((BUILDKITE_PARALLEL_JOB+1))} -export JOB=kibana-default-ciGroup${CI_GROUP} - -echo "--- Default CI Group $CI_GROUP" - -cd "$XPACK_DIR" - -checks-reporter-with-killswitch "X-Pack Chrome Functional tests / Group ${CI_GROUP}" \ - node scripts/functional_tests \ - --bail \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --include-tag "ciGroup$CI_GROUP" - -cd "$KIBANA_DIR" diff --git a/.buildkite/scripts/steps/functional/xpack_firefox.sh b/.buildkite/scripts/steps/functional/xpack_firefox.sh deleted file mode 100755 index a9a9704ea78de..0000000000000 --- a/.buildkite/scripts/steps/functional/xpack_firefox.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/steps/functional/common.sh - -cd "$XPACK_DIR" - -echo --- Default Firefox Smoke Tests - -checks-reporter-with-killswitch "X-Pack firefox smoke test" \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --include-tag "includeFirefox" \ - --config test/functional/config.firefox.js \ - --config test/functional_embedded/config.firefox.ts diff --git a/.buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh b/.buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh deleted file mode 100755 index 5e00d2446bf7a..0000000000000 --- a/.buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/common/util.sh - -.buildkite/scripts/bootstrap.sh -.buildkite/scripts/download_build_artifacts.sh - -cd "$XPACK_DIR" - -echo --- Capture Kibana Saved Objects field count metrics -checks-reporter-with-killswitch "Capture Kibana Saved Objects field count metrics" \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ - --config test/saved_objects_field_count/config.ts; diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.js b/.buildkite/scripts/steps/storybooks/build_and_upload.js index becb8f1bd871f..c541f59548753 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.js +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.js @@ -38,6 +38,7 @@ const STORYBOOKS = [ 'security_solution', 'shared_ux', 'ui_actions_enhanced', + 'unified_search', ]; const GITHUB_CONTEXT = 'Build and Publish Storybooks'; diff --git a/.buildkite/scripts/steps/test/api_integration.sh b/.buildkite/scripts/steps/test/api_integration.sh deleted file mode 100755 index f56e98903d226..0000000000000 --- a/.buildkite/scripts/steps/test/api_integration.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/common/util.sh - -is_test_execution_step - -.buildkite/scripts/bootstrap.sh - -echo --- API Integration Tests -checks-reporter-with-killswitch "API Integration Tests" \ - node scripts/functional_tests \ - --config test/api_integration/config.js \ - --bail \ - --debug diff --git a/.buildkite/scripts/steps/test/ftr_configs.sh b/.buildkite/scripts/steps/test/ftr_configs.sh new file mode 100755 index 0000000000000..244b108a269f8 --- /dev/null +++ b/.buildkite/scripts/steps/test/ftr_configs.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/steps/functional/common.sh + +export JOB_NUM=$BUILDKITE_PARALLEL_JOB +export JOB=ftr-configs-${JOB_NUM} + +FAILED_CONFIGS_KEY="${BUILDKITE_STEP_ID}${BUILDKITE_PARALLEL_JOB:-0}" + +# a FTR failure will result in the script returning an exit code of 10 +exitCode=0 + +configs="${FTR_CONFIG:-}" + +# The first retry should only run the configs that failed in the previous attempt +# Any subsequent retries, which would generally only happen by someone clicking the button in the UI, will run everything +if [[ ! "$configs" && "${BUILDKITE_RETRY_COUNT:-0}" == "1" ]]; then + configs=$(buildkite-agent meta-data get "$FAILED_CONFIGS_KEY" --default '') + if [[ "$configs" ]]; then + echo "--- Retrying only failed configs" + echo "$configs" + fi +fi + +if [[ "$configs" == "" ]]; then + echo "--- downloading ftr test run order" + buildkite-agent artifact download ftr_run_order.json . + configs=$(jq -r '.groups[env.JOB_NUM | tonumber].names | .[]' ftr_run_order.json) +fi + +failedConfigs="" +results=() + +while read -r config; do + if [[ ! "$config" ]]; then + continue; + fi + + echo "--- $ node scripts/functional_tests --bail --config $config" + start=$(date +%s) + + # prevent non-zero exit code from breaking the loop + set +e; + node ./scripts/functional_tests \ + --bail \ + --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ + --config="$config" + lastCode=$? + set -e; + + timeSec=$(($(date +%s)-start)) + if [[ $timeSec -gt 60 ]]; then + min=$((timeSec/60)) + sec=$((timeSec-(min*60))) + duration="${min}m ${sec}s" + else + duration="${timeSec}s" + fi + + results+=("- $config + duration: ${duration} + result: ${lastCode}") + + if [ $lastCode -ne 0 ]; then + exitCode=10 + echo "FTR exited with code $lastCode" + echo "^^^ +++" + + if [[ "$failedConfigs" ]]; then + failedConfigs="${failedConfigs}"$'\n'"$config" + else + failedConfigs="$config" + fi + fi +done <<< "$configs" + +if [[ "$failedConfigs" ]]; then + buildkite-agent meta-data set "$FAILED_CONFIGS_KEY" "$failedConfigs" +fi + +echo "--- FTR configs complete" +printf "%s\n" "${results[@]}" +echo "" + +exit $exitCode diff --git a/.buildkite/scripts/steps/test/jest_env.sh b/.buildkite/scripts/steps/test/jest_env.sh deleted file mode 100644 index 80e88bebba184..0000000000000 --- a/.buildkite/scripts/steps/test/jest_env.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# keys used to associate test group data in ci-stats with Jest execution order -export TEST_GROUP_TYPE_UNIT="Jest Unit Tests" -export TEST_GROUP_TYPE_INTEGRATION="Jest Integration Tests" diff --git a/.buildkite/scripts/steps/test/jest_parallel.sh b/.buildkite/scripts/steps/test/jest_parallel.sh index 86c685d82c7e7..71ecf7a853d4a 100755 --- a/.buildkite/scripts/steps/test/jest_parallel.sh +++ b/.buildkite/scripts/steps/test/jest_parallel.sh @@ -1,13 +1,12 @@ #!/bin/bash -set -uo pipefail - -source .buildkite/scripts/steps/test/jest_env.sh +set -euo pipefail export JOB=$BUILDKITE_PARALLEL_JOB # a jest failure will result in the script returning an exit code of 10 exitCode=0 +results=() if [[ "$1" == 'jest.config.js' ]]; then # run unit tests in parallel @@ -20,14 +19,32 @@ else fi export TEST_TYPE -echo "--- downloading integration test run order" +echo "--- downloading jest test run order" buildkite-agent artifact download jest_run_order.json . configs=$(jq -r 'getpath([env.TEST_TYPE]) | .groups[env.JOB | tonumber].names | .[]' jest_run_order.json) while read -r config; do echo "--- $ node scripts/jest --config $config" + start=$(date +%s) + + # prevent non-zero exit code from breaking the loop + set +e; NODE_OPTIONS="--max-old-space-size=14336" node ./scripts/jest --config="$config" "$parallelism" --coverage=false --passWithNoTests lastCode=$? + set -e; + + timeSec=$(($(date +%s)-start)) + if [[ $timeSec -gt 60 ]]; then + min=$((timeSec/60)) + sec=$((timeSec-(min*60))) + duration="${min}m ${sec}s" + else + duration="${timeSec}s" + fi + + results+=("- $config + duration: ${duration} + result: ${lastCode}") if [ $lastCode -ne 0 ]; then exitCode=10 @@ -36,4 +53,8 @@ while read -r config; do fi done <<< "$configs" +echo "--- Jest configs complete" +printf "%s\n" "${results[@]}" +echo "" + exit $exitCode diff --git a/.buildkite/scripts/steps/test/pick_jest_config_run_order.sh b/.buildkite/scripts/steps/test/pick_jest_config_run_order.sh deleted file mode 100644 index 37d4e629c90b0..0000000000000 --- a/.buildkite/scripts/steps/test/pick_jest_config_run_order.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -source .buildkite/scripts/common/util.sh -source .buildkite/scripts/steps/test/jest_env.sh - -echo '--- Pick Jest Config Run Order' -node "$(dirname "${0}")/pick_jest_config_run_order.js" diff --git a/.buildkite/scripts/steps/test/pick_jest_config_run_order.js b/.buildkite/scripts/steps/test/pick_test_group_run_order.js similarity index 100% rename from .buildkite/scripts/steps/test/pick_jest_config_run_order.js rename to .buildkite/scripts/steps/test/pick_test_group_run_order.js diff --git a/.buildkite/scripts/steps/test/pick_test_group_run_order.sh b/.buildkite/scripts/steps/test/pick_test_group_run_order.sh new file mode 100644 index 0000000000000..56308b73c6fd7 --- /dev/null +++ b/.buildkite/scripts/steps/test/pick_test_group_run_order.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh + +echo '--- Pick Test Group Run Order' +node "$(dirname "${0}")/pick_test_group_run_order.js" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7f7c048717f02..156a306b12e89 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -128,6 +128,7 @@ /src/apm.js @elastic/kibana-core @vigneshshanmugam /packages/kbn-apm-config-loader/ @elastic/kibana-core @vigneshshanmugam /src/core/types/elasticsearch @elastic/apm-ui +/packages/elastic-apm-synthtrace/ @elastic/apm-ui #CC# /src/plugins/apm_oss/ @elastic/apm-ui #CC# /x-pack/plugins/observability/ @elastic/apm-ui diff --git a/docs/api/actions-and-connectors/legacy/index.asciidoc b/docs/api/actions-and-connectors/legacy/index.asciidoc index 859dd652de984..66ecb2ed31119 100644 --- a/docs/api/actions-and-connectors/legacy/index.asciidoc +++ b/docs/api/actions-and-connectors/legacy/index.asciidoc @@ -1,4 +1,4 @@ [[actions-and-connectors-legacy-apis]] === Deprecated 7.x APIs -These APIs are deprecated and will be removed as of 8.0. +These APIs are deprecated and will be removed in a future release. diff --git a/docs/api/alerting/legacy/index.asciidoc b/docs/api/alerting/legacy/index.asciidoc index cce2c378bdb58..48f37c06ff543 100644 --- a/docs/api/alerting/legacy/index.asciidoc +++ b/docs/api/alerting/legacy/index.asciidoc @@ -1,7 +1,7 @@ [[alerts-api]] === Deprecated 7.x APIs -These APIs are deprecated and will be removed as of 8.0. +These APIs are deprecated and will be removed in a future release. include::create.asciidoc[leveloffset=+1] include::delete.asciidoc[leveloffset=+1] diff --git a/docs/developer/contributing/development-functional-tests.asciidoc b/docs/developer/contributing/development-functional-tests.asciidoc index af88754b316fa..b544add73b3b1 100644 --- a/docs/developer/contributing/development-functional-tests.asciidoc +++ b/docs/developer/contributing/development-functional-tests.asciidoc @@ -6,7 +6,7 @@ We use functional tests to make sure the {kib} UI works as expected. It replaces [discrete] === Running functional tests -The `FunctionalTestRunner` is very bare bones and gets most of its functionality from its config file, located at {blob}test/functional/config.js[test/functional/config.js] or {blob}x-pack/test/functional/config.js[x-pack/test/functional/config.js]. If you’re writing a plugin outside the {kib} repo, you will have your own config file. +The `FunctionalTestRunner` (FTR) is very bare bones and gets most of its functionality from its config file. The {kib} repo contains many FTR config files which use slightly different configurations for the {kib} server or {es}, have different test files, and potentially other config differences. You can find a manifest of all the FTR config files in `.buildkite/ftr_configs.yml`. If you’re writing a plugin outside the {kib} repo, you will have your own config file. See <> for more info. There are three ways to run the tests depending on your goals: @@ -96,7 +96,8 @@ node scripts/functional_test_runner --exclude-tag skipCloud When run without any arguments the `FunctionalTestRunner` automatically loads the configuration in the standard location, but you can override that behavior with the `--config` flag. List configs with multiple --config arguments. -* `--config test/functional/config.js` starts {es} and {kib} servers with the WebDriver tests configured to run in Chrome. +* `--config test/functional/apps/app-name/config.js` starts {es} and {kib} servers with the WebDriver tests configured to run in Chrome for a specific app. For example, +`--config test/functional/apps/home/config.js` starts {es} and {kib} servers with the WebDriver tests configured to run in Chrome for the home app. * `--config test/functional/config.firefox.js` starts {es} and {kib} servers with the WebDriver tests configured to run in Firefox. * `--config test/api_integration/config.js` starts {es} and {kib} servers with the api integration tests configuration. * `--config test/accessibility/config.ts` starts {es} and {kib} servers with the WebDriver tests configured to run an accessibility audit using https://www.deque.com/axe/[axe]. @@ -416,7 +417,7 @@ export function SomethingUsefulProvider({ getService }) { ----------- + * Re-export your provider from `services/index.js` -* Import it into `src/functional/config.js` and add it to the services config: +* Import it into `src/functional/config.base.js` and add it to the services config: + ["source","js"] ----------- diff --git a/docs/developer/plugin/external-plugin-functional-tests.asciidoc b/docs/developer/plugin/external-plugin-functional-tests.asciidoc index 55b311794f9dc..4a98ffcc5d08c 100644 --- a/docs/developer/plugin/external-plugin-functional-tests.asciidoc +++ b/docs/developer/plugin/external-plugin-functional-tests.asciidoc @@ -24,7 +24,7 @@ export default async function ({ readConfigFile }) { // read the {kib} config file so that we can utilize some of // its services and PageObjects - const kibanaConfig = await readConfigFile(resolve(REPO_ROOT, 'test/functional/config.js')); + const kibanaConfig = await readConfigFile(resolve(REPO_ROOT, 'test/functional/config.base.js')); return { // list paths to the files that contain your plugins tests diff --git a/docs/development/core/public/kibana-plugin-core-public.analyticsservicesetup.md b/docs/development/core/public/kibana-plugin-core-public.analyticsservicesetup.md index b1d059fe556b4..8cee67f0110dd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.analyticsservicesetup.md +++ b/docs/development/core/public/kibana-plugin-core-public.analyticsservicesetup.md @@ -9,5 +9,5 @@ Exposes the public APIs of the AnalyticsClient during the setup phase. Signature: ```typescript -export declare type AnalyticsServiceSetup = AnalyticsClient; +export declare type AnalyticsServiceSetup = Omit; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.analyticsservicepreboot.md b/docs/development/core/server/kibana-plugin-core-server.analyticsservicepreboot.md index d648455dde18d..e1129181dbb49 100644 --- a/docs/development/core/server/kibana-plugin-core-server.analyticsservicepreboot.md +++ b/docs/development/core/server/kibana-plugin-core-server.analyticsservicepreboot.md @@ -9,5 +9,5 @@ Exposes the public APIs of the AnalyticsClient during the preboot phase Signature: ```typescript -export declare type AnalyticsServicePreboot = AnalyticsClient; +export declare type AnalyticsServicePreboot = Omit; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.analyticsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.analyticsservicesetup.md index aa84919848f24..5dc90eb0c1f03 100644 --- a/docs/development/core/server/kibana-plugin-core-server.analyticsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.analyticsservicesetup.md @@ -9,5 +9,5 @@ Exposes the public APIs of the AnalyticsClient during the setup phase. Signature: ```typescript -export declare type AnalyticsServiceSetup = AnalyticsClient; +export declare type AnalyticsServiceSetup = Omit; ``` diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 668a6edcad3db..41321f991c1b0 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -11,7 +11,6 @@ include::{docs-root}/shared/versions/stack/{source_branch}.asciidoc[] :docker-image: {docker-repo}:{version} :es-docker-repo: docker.elastic.co/elasticsearch/elasticsearch :es-docker-image: {es-docker-repo}:{version} -:blob: {kib-repo}blob/{branch}/ :security-ref: https://www.elastic.co/community/security/ :Data-source: Data view :data-source: data view @@ -20,6 +19,8 @@ include::{docs-root}/shared/versions/stack/{source_branch}.asciidoc[] include::{docs-root}/shared/attributes.asciidoc[] +:blob: {kib-repo}blob/{branch}/ + include::user/index.asciidoc[] include::accessibility.asciidoc[] diff --git a/docs/osquery/osquery.asciidoc b/docs/osquery/osquery.asciidoc index e450bcbcb7d6f..9e384d79a4d6b 100644 --- a/docs/osquery/osquery.asciidoc +++ b/docs/osquery/osquery.asciidoc @@ -127,6 +127,8 @@ The Osquery Manager integration includes a set of prebuilt Osquery packs that yo You can modify the scheduled agent policies for a prebuilt pack, but you cannot edit queries in the pack. To edit the queries, you must first create a copy of the pack. +For information about the prebuilt packs that are available, refer to <>. + [float] [[load-prebuilt-packs]] === Load and activate prebuilt Elastic packs @@ -310,3 +312,5 @@ https://osquery.readthedocs.io/en/stable/deployment/logging/#differential-logs[d include::manage-integration.asciidoc[] include::exported-fields-reference.asciidoc[] + +include::prebuilt-packs.asciidoc[] diff --git a/docs/osquery/prebuilt-packs.asciidoc b/docs/osquery/prebuilt-packs.asciidoc new file mode 100644 index 0000000000000..776a1937b7a69 --- /dev/null +++ b/docs/osquery/prebuilt-packs.asciidoc @@ -0,0 +1,63 @@ +[[prebuilt-packs]] +== Prebuilt packs reference + +This section lists all prebuilt packs available for Osquery Manager. +Each pack is also available as a saved object, with the name `Pack: `. + +For more information, refer to <>. + + +|=== +|Name |Description |Source |Added + +|`hardware-monitoring` +|Monitor for hardware changes. +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`incident-response` +|Detect and respond to breaches. +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`it-compliance` +a|Identify outdated and vulnerable software. + +Dashboard: `[Osquery Manager] Compliance pack` + +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`osquery-monitoring` +|Monitor Osquery info and performance. +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`ossec-rootkit` +a|Run rootkit detection queries to monitor for compromise. + +Dashboard: `[Osquery Manager] OSSEC rootkit pack` + +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`osx-attacks` +|Identify compromised macOS systems. +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`unwanted-chrome-extensions` +|Monitor for malicious Chrome extensions. +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`vuln-management` +|Identify system vulnerabilities. +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 + +|`windows-attacks` +|Monitor for evidence of Windows attacks. +|https://github.com/osquery/osquery/tree/master/packs[Osquery] +|8.2 +|=== diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 33c97373a76c4..95003a08b7b09 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -92,7 +92,7 @@ URLs can use both the `ssl` and `smtp` options. + No other URL values should be part of this URL, including paths, query strings, and authentication information. When an http or smtp request -is made as part of executing an action, only the protocol, hostname, and +is made as part of running an action, only the protocol, hostname, and port of the URL for that request are used to look up these configuration values. @@ -187,33 +187,37 @@ For example, `20m`, `24h`, `7d`, `1w`. Default: `60s`. [[alert-settings]] ==== Alerting settings -`xpack.alerting.maxEphemeralActionsPerAlert`:: -Sets the number of actions that will be executed ephemerally. To use this, enable ephemeral tasks in task manager first with <> +`xpack.alerting.maxEphemeralActionsPerAlert` {ess-icon}:: +Sets the number of actions that will run ephemerally. To use this, enable +ephemeral tasks in task manager first with +<> -`xpack.alerting.cancelAlertsOnRuleTimeout`:: -Specifies whether to skip writing alerts and scheduling actions if rule execution is cancelled due to timeout. Default: `true`. This setting can be overridden by individual rule types. +`xpack.alerting.cancelAlertsOnRuleTimeout` {ess-icon}:: +Specifies whether to skip writing alerts and scheduling actions if rule +processing was cancelled due to a timeout. Default: `true`. This setting can be +overridden by individual rule types. -`xpack.alerting.rules.minimumScheduleInterval.value`:: +`xpack.alerting.rules.minimumScheduleInterval.value` {ess-icon}:: Specifies the minimum schedule interval for rules. This minimum is applied to all rules created or updated after you set this value. The time is formatted as: + `[s,m,h,d]` + -For example, `20m`, `24h`, `7d`. Default: `1m`. +For example, `20m`, `24h`, `7d`. This duration cannot exceed `1d`. Default: `1m`. -`xpack.alerting.rules.minimumScheduleInterval.enforce`:: +`xpack.alerting.rules.minimumScheduleInterval.enforce` {ess-icon}:: Specifies the behavior when a new or changed rule has a schedule interval less than the value defined in `xpack.alerting.rules.minimumScheduleInterval.value`. If `false`, rules with schedules less than the interval will be created but warnings will be logged. If `true`, rules with schedules less than the interval cannot be created. Default: `false`. -`xpack.alerting.rules.run.actions.max`:: -Specifies the maximum number of actions that a rule can trigger each time detection checks run. +`xpack.alerting.rules.run.actions.max` {ess-icon}:: +Specifies the maximum number of actions that a rule can generate each time detection checks run. -`xpack.alerting.rules.run.timeout`:: +`xpack.alerting.rules.run.timeout` {ess-icon}:: Specifies the default timeout for tasks associated with all types of rules. The time is formatted as: + `[ms,s,m,h,d,w,M,Y]` + For example, `20m`, `24h`, `7d`, `1w`. Default: `5m`. -`xpack.alerting.rules.run.ruleTypeOverrides`:: +`xpack.alerting.rules.run.ruleTypeOverrides` {ess-icon}:: Overrides the configs under `xpack.alerting.rules.run` for the rule type with the given ID. List the rule identifier and its settings in an array of objects. + For example: @@ -226,7 +230,7 @@ xpack.alerting.rules.run: timeout: '15m' -- -`xpack.alerting.rules.run.actions.connectorTypeOverrides`:: +`xpack.alerting.rules.run.actions.connectorTypeOverrides` {ess-icon}:: Overrides the configs under `xpack.alerting.rules.run.actions` for the connector type with the given ID. List the connector type identifier and its settings in an array of objects. + For example: diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index b5e0af7e188a1..d706526730459 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -63,6 +63,9 @@ Reporting generates reports in the background and jobs are coordinated using doc in {es}. Depending on how often you generate reports and the overall number of reports, you might need to change the following settings. +`xpack.reporting.capture.maxAttempts` {ess-icon}:: +If capturing a report fails for any reason, {kib} will re-queue the report job for retry, as many times as this setting. Defaults to `3`. + `xpack.reporting.queue.indexInterval`:: How often the index that stores reporting jobs rolls over to a new index. Valid values are `year`, `month`, `week`, `day`, and `hour`. Defaults to `week`. @@ -86,26 +89,23 @@ Specifies the {time-units}[time] that the reporting poller waits between polling [[reporting-capture-settings]] ==== Capture settings -Reporting works by capturing screenshots from {kib}. The following settings control the capturing process. +Reporting uses an internal "screenshotting" plugin to capture screenshots from {kib}. The following settings control the capturing process. -`xpack.reporting.capture.timeouts.openUrl` {ess-icon}:: +`xpack.screenshotting.capture.timeouts.openUrl` {ess-icon}:: Specify the {time-units}[time] to allow the Reporting browser to wait for the "Loading..." screen to dismiss and find the initial data for the page. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows a warning message. Can be specified as number of milliseconds. Defaults to `1m`. -`xpack.reporting.capture.timeouts.waitForElements` {ess-icon}:: +`xpack.screenshotting.capture.timeouts.waitForElements` {ess-icon}:: Specify the {time-units}[time] to allow the Reporting browser to wait for all visualization panels to load on the page. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows a warning message. Can be specified as number of milliseconds. Defaults to `30s`. -`xpack.reporting.capture.timeouts.renderComplete` {ess-icon}:: +`xpack.screenshotting.capture.timeouts.renderComplete` {ess-icon}:: Specify the {time-units}[time] to allow the Reporting browser to wait for all visualizations to fetch and render the data. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows a warning message. Can be specified as number of milliseconds. Defaults to `30s`. -NOTE: If any timeouts from `xpack.reporting.capture.timeouts.*` settings occur when +NOTE: If any timeouts from `xpack.screenshotting.capture.timeouts.*` settings occur when running a report job, Reporting will log the error and try to continue capturing the page with a screenshot. As a result, a download will be available, but there will likely be errors in the visualizations in the report. -`xpack.reporting.capture.maxAttempts` {ess-icon}:: -If capturing a report fails for any reason, {kib} will re-attempt other reporting job, as many times as this setting. Defaults to `3`. - -`xpack.reporting.capture.loadDelay`:: +`xpack.screenshotting.capture.loadDelay`:: deprecated:[8.0.0,This setting has no effect.] Specify the {time-units}[amount of time] before taking a screenshot when visualizations are not evented. All visualizations that ship with {kib} are evented, so this setting should not have much effect. If you are seeing empty images instead of visualizations, try increasing this value. Defaults to `3s`. *NOTE*: This setting exists for backwards compatibility, but is unused and therefore does not have an affect on reporting performance. [float] @@ -114,16 +114,16 @@ deprecated:[8.0.0,This setting has no effect.] Specify the {time-units}[amount o For PDF and PNG reports, Reporting spawns a headless Chromium browser process on the server to load and capture a screenshot of the {kib} app. When installing {kib} on Linux and Windows platforms, the Chromium binary comes bundled with the {kib} download. For Mac platforms, the Chromium binary is downloaded the first time {kib} is started. -`xpack.reporting.capture.browser.chromium.disableSandbox`:: +`xpack.screenshotting.browser.chromium.disableSandbox`:: It is recommended that you research the feasibility of enabling unprivileged user namespaces. An exception is if you are running {kib} in Docker because the container runs in a user namespace with the built-in seccomp/bpf filters. For more information, refer to <>. Defaults to `false` for all operating systems except Debian and Red Hat Linux, which use `true`. -`xpack.reporting.capture.browser.chromium.proxy.enabled`:: -Enables the proxy for Chromium to use. When set to `true`, you must also specify the `xpack.reporting.capture.browser.chromium.proxy.server` setting. Defaults to `false`. +`xpack.screenshotting.browser.chromium.proxy.enabled`:: +Enables the proxy for Chromium to use. When set to `true`, you must also specify the `xpack.screenshotting.browser.chromium.proxy.server` setting. Defaults to `false`. -`xpack.reporting.capture.browser.chromium.proxy.server`:: +`xpack.screenshotting.browser.chromium.proxy.server`:: The uri for the proxy server. Providing the username and password for the proxy server via the uri is not supported. -`xpack.reporting.capture.browser.chromium.proxy.bypass`:: +`xpack.screenshotting.browser.chromium.proxy.bypass`:: An array of hosts that should not go through the proxy server and should use a direct connection instead. Examples of valid entries are "elastic.co", "*.elastic.co", ".elastic.co", ".elastic.co:5601". [float] @@ -136,13 +136,13 @@ If the Chromium browser is asked to send a request that violates the network pol NOTE: {kib} installations are not designed to be publicly accessible over the internet. The Reporting network policy and other capabilities of the Elastic Stack security features do not change this condition. -`xpack.reporting.capture.networkPolicy`:: +`xpack.screenshotting.networkPolicy`:: Capturing a screenshot from a {kib} page involves sending out requests for all the linked web assets. For example, a Markdown visualization can show an image from a remote server. -`xpack.reporting.capture.networkPolicy.enabled`:: +`xpack.screenshotting.networkPolicy.enabled`:: When `false`, disables the *Reporting* network policy. Defaults to `true`. -`xpack.reporting.capture.networkPolicy.rules`:: +`xpack.screenshotting.networkPolicy.rules`:: A policy is specified as an array of objects that describe what to allow or deny based on a host or protocol. If a host or protocol is not specified, the rule matches any host or protocol. The rule objects are evaluated sequentially from the beginning to the end of the array, and continue until there is a matching rule. If no rules allow a request, the request is denied. @@ -150,14 +150,14 @@ The rule objects are evaluated sequentially from the beginning to the end of the [source,yaml] ------------------------------------------------------- # Only allow requests to placeholder.com -xpack.reporting.capture.networkPolicy: +xpack.screenshotting.networkPolicy: rules: [ { allow: true, host: "placeholder.com" } ] ------------------------------------------------------- [source,yaml] ------------------------------------------------------- # Only allow requests to https://placeholder.com -xpack.reporting.capture.networkPolicy: +xpack.screenshotting.networkPolicy: rules: [ { allow: true, host: "placeholder.com", protocol: "https:" } ] ------------------------------------------------------- @@ -166,7 +166,7 @@ A final `allow` rule with no host or protocol allows all requests that are not e [source,yaml] ------------------------------------------------------- # Denies requests from http://placeholder.com, but anything else is allowed. -xpack.reporting.capture.networkPolicy: +xpack.screenshotting.networkPolicy: rules: [{ allow: false, host: "placeholder.com", protocol: "http:" }, { allow: true }]; ------------------------------------------------------- @@ -175,7 +175,7 @@ A network policy can be composed of multiple rules: [source,yaml] ------------------------------------------------------- # Allow any request to http://placeholder.com but for any other host, https is required -xpack.reporting.capture.networkPolicy +xpack.screenshotting.networkPolicy rules: [ { allow: true, host: "placeholder.com", protocol: "http:" }, { allow: true, protocol: "https:" }, diff --git a/docs/settings/search-sessions-settings.asciidoc b/docs/settings/search-sessions-settings.asciidoc index 7b03cd23a9023..7628ecfb397e1 100644 --- a/docs/settings/search-sessions-settings.asciidoc +++ b/docs/settings/search-sessions-settings.asciidoc @@ -7,26 +7,26 @@ Configure the search session settings in your `kibana.yml` configuration file. -`xpack.data_enhanced.search.sessions.enabled` {ess-icon}:: +`data.search.sessions.enabled` {ess-icon}:: Set to `true` (default) to enable search sessions. -`xpack.data_enhanced.search.sessions.trackingInterval` {ess-icon}:: +`data.search.sessions.trackingInterval` {ess-icon}:: The frequency for updating the state of a search session. The default is `10s`. -`xpack.data_enhanced.search.sessions.pageSize` {ess-icon}:: +`data.search.sessions.pageSize` {ess-icon}:: How many search sessions {kib} processes at once while monitoring session progress. The default is `100`. -`xpack.data_enhanced.search.sessions.notTouchedTimeout` {ess-icon}:: +`data.search.sessions.notTouchedTimeout` {ess-icon}:: How long {kib} stores search results from unsaved sessions, after the last search in the session completes. The default is `5m`. -`xpack.data_enhanced.search.sessions.notTouchedInProgressTimeout` {ess-icon}:: +`data.search.sessions.notTouchedInProgressTimeout` {ess-icon}:: How long a search session can run after a user navigates away without saving a session. The default is `1m`. -`xpack.data_enhanced.search.sessions.maxUpdateRetries` {ess-icon}:: +`data.search.sessions.maxUpdateRetries` {ess-icon}:: How many retries {kib} can perform while attempting to save a search session. The default is `3`. -`xpack.data_enhanced.search.sessions.defaultExpiration` {ess-icon}:: +`data.search.sessions.defaultExpiration` {ess-icon}:: How long search session results are stored before they are deleted. Extending a search session resets the expiration by the same value. The default is `7d`. diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 584d45dc088fd..ca0b8ff8ee111 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -63,7 +63,7 @@ Rule schedules are defined as an interval between subsequent checks, and can ran [IMPORTANT] ============================================== -The intervals of rule checks in {kib} are approximate. The timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. +The intervals of rule checks in {kib} are approximate. Their timing is affected by factors such as the frequency at which tasks are claimed and the task load on the system. Refer to <> for more information. ============================================== [float] @@ -82,7 +82,7 @@ The result is a template: all the parameters needed to invoke a service are supp In the server monitoring example, the `email` connector type is used, and `server` is mapped to the body of the email, using the template string `CPU on {{server}} is high`. -When the rule detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` connector type. +When the rule detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and runs the action on the {kib} server by invoking the `email` connector type. image::images/what-is-an-action.svg[Actions are like templates that are rendered when an alert detects a condition] diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc index 6f8caedde3e18..2b92e8caa7ef9 100644 --- a/docs/user/alerting/alerting-setup.asciidoc +++ b/docs/user/alerting/alerting-setup.asciidoc @@ -62,7 +62,7 @@ Rules and connectors are isolated to the {kib} space in which they were created. [[alerting-authorization]] === Authorization -Rules are authorized using an <> associated with the last user to edit the rule. This API key captures a snapshot of the user's privileges at the time of edit and is subsequently used to run all background tasks associated with the rule, including condition checks, like {es} queries, and action executions. The following rule actions will re-generate the API key: +Rules are authorized using an <> associated with the last user to edit the rule. This API key captures a snapshot of the user's privileges at the time of edit and is subsequently used to run all background tasks associated with the rule, including condition checks like {es} queries and triggered actions. The following rule actions will re-generate the API key: * Creating a rule * Enabling a disabled rule diff --git a/docs/user/alerting/alerting-troubleshooting.asciidoc b/docs/user/alerting/alerting-troubleshooting.asciidoc index 5f3c566e82d42..32c77d7fa57a7 100644 --- a/docs/user/alerting/alerting-troubleshooting.asciidoc +++ b/docs/user/alerting/alerting-troubleshooting.asciidoc @@ -1,11 +1,7 @@ -[role="xpack"] [[alerting-troubleshooting]] -== Troubleshooting -++++ -Troubleshooting -++++ +== Troubleshooting and limitations -Alerting provides many options for diagnosing problems with Rules and Connectors. +Alerting provides many options for diagnosing problems with rules and connectors. [float] [[alerting-kibana-log]] @@ -56,7 +52,7 @@ Diagnosing these may be difficult - but there may be log messages for error cond === Use the REST APIs There is a rich set of HTTP endpoints to introspect and manage rules and connectors. -One of the http endpoints available for actions is the POST <>. You can use this to “test” an action. For instance, if you have a server log action created, you can execute it via curling the endpoint: +One of the http endpoints available for actions is the POST <>. You can use this to “test” an action. For instance, if you have a server log action created, you can run it via curling the endpoint: [source, txt] -------------------------------------------------- curl -X POST -k \ @@ -79,13 +75,13 @@ The same REST POST _execute API command will be: kbn-action execute a692dc89-15b9-4a3c-9e47-9fb6872e49ce ‘{"params":{"subject":"hallo","message":"hallo!","to":["me@example.com"]}}’ -------------------------------------------------- -The result of this http request (and printed to stdout by https://github.com/pmuellr/kbn-action[kbn-action]) will be data returned by the action execution, along with error messages if errors were encountered. +The result of this http request (and printed to stdout by https://github.com/pmuellr/kbn-action[kbn-action]) will be data returned by the action, along with error messages if errors were encountered. [float] [[alerting-error-banners]] === Look for error banners -The Rule Management and Rule Details pages contain an error banner, which helps to identify the errors for the rules: +The *Rule Management* and *Rule Details* pages contain an error banner, which helps to identify the errors for the rules: [role="screenshot"] image::images/rules-management-health.png[Rule management page with the errors banner] @@ -96,14 +92,14 @@ image::images/rules-details-health.png[Rule details page with the errors banner] [[task-manager-diagnostics]] === Task Manager diagnostics -Under the hood, Rules and Connectors uses a plugin called Task Manager, which handles the scheduling, execution, and error handling of the tasks. -This means that failure cases in Rules or Connectors will, at times, be revealed by the Task Manager mechanism, rather than the Rules mechanism. +Under the hood, {rules-ui} uses a plugin called Task Manager, which handles the scheduling, running, and error handling of the tasks. +This means that failure cases in {rules-ui} will, at times, be revealed by the Task Manager mechanism, rather than the Rules mechanism. Task Manager provides a visible status which can be used to diagnose issues and is very well documented <> and <>. Task Manager uses the `.kibana_task_manager` index, an internal index that contains all the saved objects that represent the tasks in the system. [float] -==== Getting from a Rule to its Task +==== Getting from a rule to its task When a rule is created, a task is created, scheduled to run at the interval specified. For example, when a rule is created and configured to check every 5 minutes, then the underlying task will be expected to run every 5 minutes. In practice, after each time the rule runs, the task is scheduled to run again in 5 minutes, rather than being scheduled to run every 5 minutes indefinitely. If you use the <> to fetch the underlying rule, you’ll get an object like so: @@ -190,12 +186,28 @@ When diagnosing the health state of the task, you will most likely be interested Investigating the underlying task can help you gauge whether the problem you’re seeing is rooted in the rule not running at all, whether it’s running and failing, or whether it is running, but exhibiting behavior that is different than what was expected (at which point you should focus on the rule itself, rather than the task). -In addition to the above methods, broadly used the next approaches and common issues: +In addition to the above methods, refer to the following approaches and common issues: * <> * <> * <> +[discrete] +[[alerting-limitations]] +=== Limitations + +The following limitations and known problems apply to the {version} release of +the {kib} {alert-features}. + +[discrete] +==== Alert visibility + +If you create a rule in the {observability} or {security-app}, its alerts are +not visible in *{stack-manage-app} > {rules-ui}*. You can view them only in the +{kib} app where you created the rule. If you use the +<>, the visibility of the alerts is related to +the `consumer` property. + include::troubleshooting/alerting-common-issues.asciidoc[] include::troubleshooting/event-log-index.asciidoc[] include::troubleshooting/testing-connectors.asciidoc[] diff --git a/docs/user/alerting/create-and-manage-rules.asciidoc b/docs/user/alerting/create-and-manage-rules.asciidoc index ba1629abd9c86..52db2ed51217e 100644 --- a/docs/user/alerting/create-and-manage-rules.asciidoc +++ b/docs/user/alerting/create-and-manage-rules.asciidoc @@ -44,7 +44,7 @@ Notify:: This value limits how often actions are repeated when an alert rem [[alerting-concepts-suppressing-duplicate-notifications]] [NOTE] ============================================== -Since actions are executed per alert, a rule can end up generating a large number of actions. Take the following example where a rule is monitoring three servers every minute for CPU usage > 0.9, and the rule is set to notify **Every time alert is active**: +Since actions are triggered per alert, a rule can end up generating a large number of actions. Take the following example where a rule is monitoring three servers every minute for CPU usage > 0.9, and the rule is set to notify **Every time alert is active**: * Minute 1: server X123 > 0.9. *One email* is sent for server X123. * Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, one for X123 and one for Y456. @@ -163,8 +163,8 @@ A rule can have one of the following statuses: `active`:: The conditions for the rule have been met, and the associated actions should be invoked. `ok`:: The conditions for the rule have not been met, and the associated actions are not invoked. -`error`:: An error was encountered during rule execution. -`pending`:: The rule has not yet executed. The rule was either just created, or enabled after being disabled. +`error`:: An error was encountered by the rule. +`pending`:: The rule has not yet run. The rule was either just created, or enabled after being disabled. `unknown`:: A problem occurred when calculating the status. Most likely, something went wrong with the alerting code. [float] diff --git a/docs/user/alerting/rule-types.asciidoc b/docs/user/alerting/rule-types.asciidoc index 324347f3b2648..120c580330b9f 100644 --- a/docs/user/alerting/rule-types.asciidoc +++ b/docs/user/alerting/rule-types.asciidoc @@ -41,6 +41,12 @@ see {subscriptions}[the subscription page]. Observability rules are categorized into APM and User Experience, Logs, Metrics, Stack Monitoring, and Uptime. +[NOTE] +============================================== +If you create a rule in the {observability} app, its alerts are not visible in +*{stack-manage-app} > {rules-ui}*. They are visible only in the {observability} app. +============================================== + [cols="2*<"] |=== @@ -72,7 +78,13 @@ beta:[] {ml-docs}/ml-configuring-alerts.html[{ml-cap} rules] run scheduled check [[security-rules]] === Security rules -Security rules detect suspicious source events with pre-built or custom rules and create alerts when a rule’s conditions are met. For more information, refer to {security-guide}/prebuilt-rules.html[Security rules]. +Security rules detect suspicious source events with pre-built or custom rules and create alerts when a rule's conditions are met. For more information, refer to {security-guide}/prebuilt-rules.html[Security rules]. + +[NOTE] +============================================== +Alerts associated with security rules are visible only in the {security-app}; +they are not visible in *{stack-manage-app} > {rules-ui}*. +============================================== include::rule-types/index-threshold.asciidoc[] include::rule-types/es-query.asciidoc[] diff --git a/docs/user/alerting/rule-types/es-query.asciidoc b/docs/user/alerting/rule-types/es-query.asciidoc index dba8a4878cb26..c8f98808ca552 100644 --- a/docs/user/alerting/rule-types/es-query.asciidoc +++ b/docs/user/alerting/rule-types/es-query.asciidoc @@ -26,7 +26,7 @@ Index:: Specifies an *index or data view* and a *time field* that is used for the *time window*. Size:: Specifies the number of documents to pass to the configured actions when the threshold condition is met. -{es} query:: Specifies the ES DSL query to execute. The number of documents that +{es} query:: Specifies the ES DSL query. The number of documents that match this query is evaluated against the threshold condition. Only the `query` field is used, other DSL fields are not considered. Threshold:: Defines a threshold value and a comparison operator (`is above`, @@ -81,7 +81,7 @@ image::images/rule-types-es-query-example-action-variable.png[Iterate over hits Use the *Test query* feature to verify that your query DSL is valid. -* Valid queries are executed against the configured *index* using the configured +* Valid queries are run against the configured *index* using the configured *time window*. The number of documents that match the query is displayed. + [role="screenshot"] @@ -95,16 +95,14 @@ image::user/alerting/images/rule-types-es-query-invalid.png[Test {es} query show [float] ==== Handling multiple matches of the same document -This rule type checks for duplication of document matches across rule -executions. If you configure the rule with a schedule interval smaller than the -time window, and a document matches a query in multiple rule executions, it is -alerted on only once. +This rule type checks for duplication of document matches across multiple runs. +If you configure the rule with a schedule interval smaller than the time window, +and a document matches a query in multiple runs, it is alerted on only once. The rule uses the timestamp of the matches to avoid alerting on the same match multiple times. The timestamp of the latest match is used for evaluating the -rule conditions when the rule is executed. Only matches between the latest -timestamp from the previous execution and the actual rule execution are -considered. +rule conditions when the rule runs. Only matches between the latest timestamp +from the previous run and the current run are considered. Suppose you have a rule configured to run every minute. The rule uses a time window of 1 hour and checks if there are more than 99 matches for the query. The @@ -112,16 +110,16 @@ window of 1 hour and checks if there are more than 99 matches for the query. The [cols="3*<"] |=== -| `Execution 1 (0:00)` +| `Run 1 (0:00)` | Rule finds 113 matches in the last hour: `113 > 99` | Rule is active and user is alerted. -| `Execution 2 (0:01)` +| `Run 2 (0:01)` | Rule finds 127 matches in the last hour. 105 of the matches are duplicates that were already alerted on previously, so you actually have 22 matches: `22 !> 99` | No alert. -| `Execution 3 (0:02)` +| `Run 3 (0:02)` | Rule finds 159 matches in the last hour. 88 of the matches are duplicates that were already alerted on previously, so you actually have 71 matches: `71 !> 99` | No alert. -| `Execution 4 (0:03)` +| `Run 4 (0:03)` | Rule finds 190 matches in the last hour. 71 of them are duplicates that were already alerted on previously, so you actually have 119 matches: `119 > 99` | Rule is active and user is alerted. |=== \ No newline at end of file diff --git a/docs/user/alerting/rule-types/index-threshold.asciidoc b/docs/user/alerting/rule-types/index-threshold.asciidoc index c65b0f66b1b63..03f855a861022 100644 --- a/docs/user/alerting/rule-types/index-threshold.asciidoc +++ b/docs/user/alerting/rule-types/index-threshold.asciidoc @@ -52,7 +52,7 @@ In this example, you will use the {kib} < Rules and Connectors**. -. Create a new rule that is checked every four hours and executes actions when the rule status changes. +. Create a new rule that is checked every four hours and triggers actions when the rule status changes. + [role="screenshot"] image::user/alerting/images/rule-types-index-threshold-select.png[Choosing an index threshold rule type] diff --git a/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc b/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc index 7ab34dacacd98..75a158e6d364f 100644 --- a/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc +++ b/docs/user/alerting/troubleshooting/alerting-common-issues.asciidoc @@ -40,35 +40,34 @@ When diagnosing issues related to alerting, focus on the tasks that begin with ` Alerting tasks always begin with `alerting:`. For example, the `alerting:.index-threshold` tasks back the <>. Action tasks always begin with `actions:`. For example, the `actions:.index` tasks back the <>. -For more details on monitoring and diagnosing task execution in Task Manager, see <>. +For more details on monitoring and diagnosing tasks in Task Manager, refer to <>. [float] [[connector-tls-settings]] -==== Connectors have TLS errors when executing actions +==== Connectors have TLS errors when running actions *Problem* -When executing actions, a connector gets a TLS socket error when connecting to -the server. +A connector gets a TLS socket error when connecting to the server to run an action. *Solution* Configuration options are available to specialize connections to TLS servers, -including ignoring server certificate validation, and providing certificate -authority data to verify servers using custom certificates. For more details, -see <>. +including ignoring server certificate validation and providing certificate +authority data to verify servers using custom certificates. For more details, +see <>. [float] -[[rules-long-execution-time]] +[[rules-long-run-time]] ==== Rules take a long time to run *Problem* -Rules are taking a long time to execute and are impacting the overall health of your deployment. +Rules are taking a long time to run and are impacting the overall health of your deployment. [IMPORTANT] ============================================== -By default, only users with a `superuser` role can query the experimental[] {kib} event log because it is a system index. To enable additional users to execute this query, assign `read` privileges to the `.kibana-event-log*` index. +By default, only users with a `superuser` role can query the experimental[] {kib} event log because it is a system index. To enable additional users to run this query, assign `read` privileges to the `.kibana-event-log*` index. ============================================== *Solution* @@ -87,9 +86,9 @@ image::images/rule-details-timeout-error.png[Rule details page with timeout erro If you want your rules to run longer, update the `xpack.alerting.rules.run.timeout` configuration in your <>. You can also target a specific rule type by using `xpack.alerting.rules.run.ruleTypeOverrides`. -Rules that consistently run longer than their <> may produce unexpected results. If the average run duration, visible on the <>, is greater than the check interval, consider increasing the check interval. +Rules that consistently run longer than their <> may produce unexpected results. If the average run duration, visible on the <>, is greater than the check interval, consider increasing the check interval. -To get all long-running rules, you can query for a list of rule ids, bucketed by their execution times: +To get all long-running rules, you can query for a list of rule ids, bucketed by their run times: [source,console] -------------------------------------------------- @@ -160,9 +159,9 @@ GET /.kibana-event-log*/_search -------------------------------------------------- // TEST -<1> This queries for rules executed in the last day. Update the values of `lte` and `gte` to query over a different time range. -<2> Use `event.provider: actions` to query for long-running action executions. -<3> Execution durations are stored as nanoseconds. This adds a runtime field to convert that duration into seconds. +<1> This queries for rules run in the last day. Update the values of `lte` and `gte` to query over a different time range. +<2> Use `event.provider: actions` to query for long-running actions. +<3> Run durations are stored as nanoseconds. This adds a runtime field to convert that duration into seconds. <4> This interval buckets the `event.duration_in_seconds` runtime field into 1 second intervals. Update this value to change the granularity of the buckets. If you are unable to use runtime fields, make sure this aggregation targets `event.duration` and use nanoseconds for the interval. <5> This retrieves the top 10 rule ids for this duration interval. Update this value to retrieve more rule ids. @@ -237,10 +236,10 @@ This query returns the following: } } -------------------------------------------------- -<1> Most rule execution durations fall within the first bucket (0 - 1 seconds). -<2> A single rule with id `41893910-6bca-11eb-9e0d-85d233e3ee35` took between 30 and 31 seconds to execute. +<1> Most run durations fall within the first bucket (0 - 1 seconds). +<2> A single rule with id `41893910-6bca-11eb-9e0d-85d233e3ee35` took between 30 and 31 seconds to run. -Use the <> to retrieve additional information about rules that take a long time to execute. +Use the <> to retrieve additional information about rules that take a long time to run. [float] [[rule-cannot-decrypt-api-key]] @@ -248,11 +247,11 @@ Use the <> to retrieve additional information about r *Problem*: -The rule fails to execute and has an `Unable to decrypt attribute "apiKey"` error. +The rule fails to run and has an `Unable to decrypt attribute "apiKey"` error. *Solution*: -This error happens when the `xpack.encryptedSavedObjects.encryptionKey` value used to create the rule does not match the value used during rule execution. Depending on the scenario, there are different ways to solve this problem: +This error happens when the `xpack.encryptedSavedObjects.encryptionKey` value used to create the rule does not match the value used when the rule runs. Depending on the scenario, there are different ways to solve this problem: [cols="2*<"] |=== diff --git a/docs/user/alerting/troubleshooting/event-log-index.asciidoc b/docs/user/alerting/troubleshooting/event-log-index.asciidoc index 5016b6d6f19c9..a0e6cd11ed184 100644 --- a/docs/user/alerting/troubleshooting/event-log-index.asciidoc +++ b/docs/user/alerting/troubleshooting/event-log-index.asciidoc @@ -6,15 +6,16 @@ experimental[] Use the event log index to determine: -* Whether a rule successfully ran but its associated actions did not +* Whether a rule ran successfully but its associated actions did not * Whether a rule was ever activated -* Additional information about rule execution errors -* Duration times for rule and action executions +* Additional information about errors when the rule ran +* Run durations for the rules and actions [float] -==== Example Event Log Queries +==== Example event log queries + +The following event log query looks at all events related to a specific rule id: -Event log query to look at all event related to a specific rule id: [source, txt] -------------------------------------------------- GET /.kibana-event-log*/_search @@ -77,7 +78,9 @@ GET /.kibana-event-log*/_search } -------------------------------------------------- -Event log query to look at all events related to executing a rule or action. These events include duration. +The following event log query looks at all events related to running a rule or +action. These events include duration: + [source, txt] -------------------------------------------------- GET /.kibana-event-log*/_search @@ -124,8 +127,10 @@ GET /.kibana-event-log*/_search } -------------------------------------------------- -Event log query to look at the errors. -You should see an `error.message` property in that event, with a message from the action executor that might provide more detail on why the action encountered an error: +The following event log query looks at the errors. You should see an +`error.message` property in that event, with a message that might provide more +details about why the action encountered an error: + [source, txt] -------------------------------------------------- { @@ -150,7 +155,9 @@ You should see an `error.message` property in that event, with a message from th } -------------------------------------------------- -And see the errors for the rules you might provide the next search query: +You might also see the errors for the rules, which can use in the next search +query. For example: + [source, txt] -------------------------------------------------- { diff --git a/docs/user/alerting/troubleshooting/testing-connectors.asciidoc b/docs/user/alerting/troubleshooting/testing-connectors.asciidoc index 64ba106655321..fd5a897dfd4c3 100644 --- a/docs/user/alerting/troubleshooting/testing-connectors.asciidoc +++ b/docs/user/alerting/troubleshooting/testing-connectors.asciidoc @@ -15,9 +15,10 @@ image::user/alerting/images/email-connector-test.png[Rule management page with t image::user/alerting/images/teams-connector-test.png[Five clauses define the condition to detect] [float] -==== experimental[] Troubleshooting Connectors with `kbn-action` tool +==== experimental[] Troubleshooting connectors with the `kbn-action` tool -Executing an Email action via https://github.com/pmuellr/kbn-action[kbn-action]. In this example, is using a cloud deployment of the stack: +You can run an email action via https://github.com/pmuellr/kbn-action[kbn-action]. +In this example, it is a Cloud deployment of the {stack}: [source, txt] -------------------------------------------------- @@ -44,7 +45,8 @@ $ kbn-action ls } ] -------------------------------------------------- -and then execute this: + +You can then run the following test: [source, txt] -------------------------------------------------- diff --git a/docs/user/production-considerations/alerting-production-considerations.asciidoc b/docs/user/production-considerations/alerting-production-considerations.asciidoc index 42f9a17cc6f88..f94ec1c3d04bb 100644 --- a/docs/user/production-considerations/alerting-production-considerations.asciidoc +++ b/docs/user/production-considerations/alerting-production-considerations.asciidoc @@ -64,3 +64,50 @@ Because {kib} uses the documents to display historic data, you should set the de For more information on index lifecycle management, see: {ref}/index-lifecycle-management.html[Index Lifecycle Policies]. + +[float] +[[alerting-circuit-breakers]] +=== Circuit breakers + +There are several scenarios where running alerting rules and actions can start to negatively impact the overall health of a {kib} instance either by clogging up Task Manager throughput or by consuming so much CPU/memory that other operations cannot complete in a reasonable amount of time. There are several <> circuit breakers to help minimize these effects. + +[float] +==== Rules with very short intervals + +Running large numbers of rules at very short intervals can quickly clog up Task Manager throughput, leading to higher schedule drift. Use `xpack.alerting.rules.minimumScheduleInterval.value` to set a minimum schedule interval for rules. The default (and recommended) value for this configuration is `1m`. Use `xpack.alerting.rules.minimumScheduleInterval.enforce` to specify whether to strictly enforce this minimum. While the default value for this setting is `false` to maintain backwards compatibility with existing rules, set this to `true` to prevent new and updated rules from running at an interval below the minimum. + +[float] +==== Rules that run for a long time + +Rules that run for a long time typically do so because they are issuing resource-intensive {es} queries or performing CPU-intensive processing. This can block the event loop, making {kib} inaccessible while the rule runs. By default, rule processing is cancelled after `5m` but this can be overriden using the `xpack.alerting.rules.run.timeout` configuration. This value can also be configured per rule type using `xpack.alerting.rules.run.ruleTypeOverrides`. For example, the following configuration sets the global timeout value to `1m` while allowing *Index Threshold* rules to run for `10m` before being cancelled. + +[source,yaml] +-- +xpack.alerting.rules.run: + timeout: '1m' + ruleTypeOverrides: + - id: '.index-threshold' + timeout: '10m' +-- + +When a rule run is cancelled, any alerts and actions that were generated during the run are discarded. This behavior is controlled by the `xpack.alerting.cancelAlertsOnRuleTimeout` configuration, which defaults to `true`. Set this to `false` to receive alerts and actions after the timeout, although be aware that these may be incomplete and possibly inaccurate. + +[float] +==== Rules that spawn too many actions + +Rules that spawn too many actions can quickly clog up Task Manager throughput. This can occur if: + +* A rule configured with a single action generates many alerts. For example, if a rule configured to run a single email action generates 100,000 alerts, then 100,000 actions will be scheduled during a run. +* A rule configured with multiple actions generates alerts. For example, if a rule configured to run an email action, a server log action and a webhook action generates 30,000 alerts, then 90,000 actions will be scheduled during a run. + +Use `xpack.alerting.rules.run.actions.max` to limit the maximum number of actions a rule can generate per run. This value can also be configured by connector type using `xpack.alerting.rules.run.actions.connectorTypeOverrides`. For example, the following config sets the global maximum number of actions to 100 while allowing rules with *Email* actions to generate up to 200 actions. + +[source,yaml] +-- +xpack.alerting.rules.run: + actions: + max: 100 + connectorTypeOverrides: + - id: '.email' + max: 200 +-- \ No newline at end of file diff --git a/docs/user/production-considerations/reporting-production-considerations.asciidoc b/docs/user/production-considerations/reporting-production-considerations.asciidoc index f673c4538f443..20ea82be9c6e4 100644 --- a/docs/user/production-considerations/reporting-production-considerations.asciidoc +++ b/docs/user/production-considerations/reporting-production-considerations.asciidoc @@ -31,10 +31,10 @@ distributions don't have user namespaces enabled by default, or they require the automatically disable the sandbox when it is running on Debian because additional steps are required to enable unprivileged usernamespaces. In these situations, you'll see the following message in your {kib} startup logs: `Chromium sandbox provides an additional layer of protection, but is not supported for your OS. -Automatically setting 'xpack.reporting.capture.browser.chromium.disableSandbox: true'.` +Automatically setting 'xpack.screenshotting.browser.chromium.disableSandbox: true'.` Reporting automatically enables the Chromium sandbox at startup when a supported OS is detected. However, if your kernel is 3.8 or newer, it's -recommended to set `xpack.reporting.capture.browser.chromium.disableSandbox: false` in your `kibana.yml` to explicitly enable usernamespaces. +recommended to set `xpack.screenshotting.browser.chromium.disableSandbox: false` in your `kibana.yml` to explicitly enable usernamespaces. [float] [[reporting-docker-sandbox]] diff --git a/nav-kibana-dev.docnav.json b/nav-kibana-dev.docnav.json index 43b23fd084673..4a1fd848a928e 100644 --- a/nav-kibana-dev.docnav.json +++ b/nav-kibana-dev.docnav.json @@ -100,7 +100,6 @@ { "id": "kibCoreSavedObjectsPluginApi" }, { "id": "kibFieldFormatsPluginApi" }, { "id": "kibDataPluginApi" }, - { "id": "kibDataAutocompletePluginApi" }, { "id": "kibDataEnhancedPluginApi" }, { "id": "kibDataViewsPluginApi" }, { "id": "kibDataQueryPluginApi" }, diff --git a/package.json b/package.json index 1b785b1fcaaab..2f0a37ff47735 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,9 @@ "@kbn/ambient-ui-types": "link:bazel-bin/packages/kbn-ambient-ui-types", "@kbn/analytics": "link:bazel-bin/packages/kbn-analytics", "@kbn/analytics-client": "link:bazel-bin/packages/analytics/client", + "@kbn/analytics-shippers-elastic-v3-browser": "link:bazel-bin/packages/analytics/shippers/elastic_v3/browser", + "@kbn/analytics-shippers-elastic-v3-common": "link:bazel-bin/packages/analytics/shippers/elastic_v3/common", + "@kbn/analytics-shippers-elastic-v3-server": "link:bazel-bin/packages/analytics/shippers/elastic_v3/server", "@kbn/analytics-shippers-fullstory": "link:bazel-bin/packages/analytics/shippers/fullstory", "@kbn/apm-config-loader": "link:bazel-bin/packages/kbn-apm-config-loader", "@kbn/apm-utils": "link:bazel-bin/packages/kbn-apm-utils", @@ -249,7 +252,7 @@ "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "del": "^5.1.0", - "elastic-apm-node": "^3.32.0", + "elastic-apm-node": "^3.33.0", "email-addresses": "^5.0.0", "execa": "^4.0.2", "exit-hook": "^2.2.0", @@ -497,6 +500,7 @@ "@kbn/import-resolver": "link:bazel-bin/packages/kbn-import-resolver", "@kbn/jest-serializers": "link:bazel-bin/packages/kbn-jest-serializers", "@kbn/optimizer": "link:bazel-bin/packages/kbn-optimizer", + "@kbn/performance-testing-dataset-extractor": "link:bazel-bin/packages/kbn-performance-testing-dataset-extractor", "@kbn/plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator", "@kbn/plugin-helpers": "link:bazel-bin/packages/kbn-plugin-helpers", "@kbn/pm": "link:packages/kbn-pm", @@ -605,6 +609,9 @@ "@types/kbn__alerts": "link:bazel-bin/packages/kbn-alerts/npm_module_types", "@types/kbn__analytics": "link:bazel-bin/packages/kbn-analytics/npm_module_types", "@types/kbn__analytics-client": "link:bazel-bin/packages/analytics/client/npm_module_types", + "@types/kbn__analytics-shippers-elastic-v3-browser": "link:bazel-bin/packages/analytics/shippers/elastic_v3/browser/npm_module_types", + "@types/kbn__analytics-shippers-elastic-v3-common": "link:bazel-bin/packages/analytics/shippers/elastic_v3/common/npm_module_types", + "@types/kbn__analytics-shippers-elastic-v3-server": "link:bazel-bin/packages/analytics/shippers/elastic_v3/server/npm_module_types", "@types/kbn__analytics-shippers-fullstory": "link:bazel-bin/packages/analytics/shippers/fullstory/npm_module_types", "@types/kbn__apm-config-loader": "link:bazel-bin/packages/kbn-apm-config-loader/npm_module_types", "@types/kbn__apm-utils": "link:bazel-bin/packages/kbn-apm-utils/npm_module_types", @@ -640,6 +647,7 @@ "@types/kbn__mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl/npm_module_types", "@types/kbn__monaco": "link:bazel-bin/packages/kbn-monaco/npm_module_types", "@types/kbn__optimizer": "link:bazel-bin/packages/kbn-optimizer/npm_module_types", + "@types/kbn__performance-testing-dataset-extractor": "link:bazel-bin/packages/kbn-performance-testing-dataset-extractor/npm_module_types", "@types/kbn__plugin-discovery": "link:bazel-bin/packages/kbn-plugin-discovery/npm_module_types", "@types/kbn__plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator/npm_module_types", "@types/kbn__plugin-helpers": "link:bazel-bin/packages/kbn-plugin-helpers/npm_module_types", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 43c80fc882159..45f57b4c4bed0 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -10,6 +10,9 @@ filegroup( name = "build_pkg_code", srcs = [ "//packages/analytics/client:build", + "//packages/analytics/shippers/elastic_v3/browser:build", + "//packages/analytics/shippers/elastic_v3/common:build", + "//packages/analytics/shippers/elastic_v3/server:build", "//packages/analytics/shippers/fullstory:build", "//packages/elastic-apm-synthtrace:build", "//packages/elastic-eslint-config-kibana:build", @@ -59,6 +62,7 @@ filegroup( "//packages/kbn-mapbox-gl:build", "//packages/kbn-monaco:build", "//packages/kbn-optimizer:build", + "//packages/kbn-performance-testing-dataset-extractor:build", "//packages/kbn-plugin-discovery:build", "//packages/kbn-plugin-generator:build", "//packages/kbn-plugin-helpers:build", @@ -114,6 +118,9 @@ filegroup( name = "build_pkg_types", srcs = [ "//packages/analytics/client:build_types", + "//packages/analytics/shippers/elastic_v3/browser:build_types", + "//packages/analytics/shippers/elastic_v3/common:build_types", + "//packages/analytics/shippers/elastic_v3/server:build_types", "//packages/analytics/shippers/fullstory:build_types", "//packages/elastic-apm-synthtrace:build_types", "//packages/elastic-safer-lodash-set:build_types", @@ -154,6 +161,7 @@ filegroup( "//packages/kbn-mapbox-gl:build_types", "//packages/kbn-monaco:build_types", "//packages/kbn-optimizer:build_types", + "//packages/kbn-performance-testing-dataset-extractor:build_types", "//packages/kbn-plugin-discovery:build_types", "//packages/kbn-plugin-generator:build_types", "//packages/kbn-plugin-helpers:build_types", diff --git a/packages/analytics/client/BUILD.bazel b/packages/analytics/client/BUILD.bazel index af79d33ccc692..0b6a811adc22c 100644 --- a/packages/analytics/client/BUILD.bazel +++ b/packages/analytics/client/BUILD.bazel @@ -62,6 +62,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -86,7 +93,7 @@ ts_project( js_library( name = PKG_DIRNAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/analytics/client/package.json b/packages/analytics/client/package.json index 82ab42bc22a31..9f2c648183a41 100644 --- a/packages/analytics/client/package.json +++ b/packages/analytics/client/package.json @@ -2,6 +2,7 @@ "name": "@kbn/analytics-client", "private": true, "version": "1.0.0", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "license": "SSPL-1.0 OR Elastic License 2.0" } diff --git a/packages/analytics/client/src/analytics_client/analytics_client.test.ts b/packages/analytics/client/src/analytics_client/analytics_client.test.ts index 1e8d5aa5445c0..5348ffa1979fa 100644 --- a/packages/analytics/client/src/analytics_client/analytics_client.test.ts +++ b/packages/analytics/client/src/analytics_client/analytics_client.test.ts @@ -9,6 +9,7 @@ // eslint-disable-next-line max-classes-per-file import type { Observable } from 'rxjs'; import { BehaviorSubject, firstValueFrom, lastValueFrom, Subject } from 'rxjs'; +import { fakeSchedulers } from 'rxjs-marbles/jest'; import type { MockedLogger } from '@kbn/logging-mocks'; import { loggerMock } from '@kbn/logging-mocks'; import { AnalyticsClient } from './analytics_client'; @@ -17,14 +18,12 @@ import { shippersMock } from '../shippers/mocks'; import type { EventContext, TelemetryCounter } from '../events'; import { TelemetryCounterType } from '../events'; -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -// FLAKY: https://github.com/elastic/kibana/issues/131369 -describe.skip('AnalyticsClient', () => { +describe('AnalyticsClient', () => { let analyticsClient: AnalyticsClient; let logger: MockedLogger; beforeEach(() => { + jest.useFakeTimers(); logger = loggerMock.create(); analyticsClient = new AnalyticsClient({ logger, @@ -33,6 +32,11 @@ describe.skip('AnalyticsClient', () => { }); }); + afterEach(() => { + analyticsClient.shutdown(); + jest.useRealTimers(); + }); + describe('registerEventType', () => { test('successfully registers a event type', () => { analyticsClient.registerEventType({ @@ -282,13 +286,16 @@ describe.skip('AnalyticsClient', () => { constructor({ optInMock, extendContextMock, + shutdownMock, }: { optInMock?: jest.Mock; extendContextMock?: jest.Mock; + shutdownMock?: jest.Mock; }) { super(); if (optInMock) this.optIn = optInMock; if (extendContextMock) this.extendContext = extendContextMock; + if (shutdownMock) this.shutdown = shutdownMock; } } @@ -311,54 +318,76 @@ describe.skip('AnalyticsClient', () => { expect(optIn).toHaveBeenCalledWith(true); }); - test('Spreads the context updates to the shipper (only after opt-in)', async () => { - const extendContextMock = jest.fn(); - analyticsClient.registerShipper(MockedShipper, { extendContextMock }); - expect(extendContextMock).toHaveBeenCalledTimes(0); // Not until we have opt-in - analyticsClient.optIn({ global: { enabled: true } }); - await delay(10); - expect(extendContextMock).toHaveBeenCalledWith({}); // The initial context - - const context$ = new Subject<{ a_field: boolean }>(); - analyticsClient.registerContextProvider({ - name: 'contextProviderA', - schema: { - a_field: { - type: 'boolean', - _meta: { - description: 'a_field description', + test( + 'Spreads the context updates to the shipper (only after opt-in)', + fakeSchedulers((advance) => { + const extendContextMock = jest.fn(); + analyticsClient.registerShipper(MockedShipper, { extendContextMock }); + expect(extendContextMock).toHaveBeenCalledTimes(0); // Not until we have opt-in + analyticsClient.optIn({ global: { enabled: true } }); + advance(10); + expect(extendContextMock).toHaveBeenCalledWith({}); // The initial context + + const context$ = new Subject<{ a_field: boolean }>(); + analyticsClient.registerContextProvider({ + name: 'contextProviderA', + schema: { + a_field: { + type: 'boolean', + _meta: { + description: 'a_field description', + }, }, }, - }, - context$, - }); - - context$.next({ a_field: true }); - expect(extendContextMock).toHaveBeenCalledWith({ a_field: true }); // After update - }); - - test('Does not spread the context if opt-in === false', async () => { - const extendContextMock = jest.fn(); - analyticsClient.registerShipper(MockedShipper, { extendContextMock }); - expect(extendContextMock).toHaveBeenCalledTimes(0); // Not until we have opt-in - analyticsClient.optIn({ global: { enabled: false } }); - await delay(10); - expect(extendContextMock).toHaveBeenCalledTimes(0); // Not until we have opt-in - }); + context$, + }); - test('Handles errors in the shipper', async () => { - const extendContextMock = jest.fn().mockImplementation(() => { - throw new Error('Something went terribly wrong'); - }); - analyticsClient.registerShipper(MockedShipper, { extendContextMock }); - analyticsClient.optIn({ global: { enabled: true } }); - await delay(10); - expect(extendContextMock).toHaveBeenCalledWith({}); // The initial context - expect(logger.warn).toHaveBeenCalledWith( - `Shipper "${MockedShipper.shipperName}" failed to extend the context`, - expect.any(Error) - ); - }); + context$.next({ a_field: true }); + expect(extendContextMock).toHaveBeenCalledWith({ a_field: true }); // After update + }) + ); + + test( + 'Does not spread the context if opt-in === false', + fakeSchedulers((advance) => { + const extendContextMock = jest.fn(); + analyticsClient.registerShipper(MockedShipper, { extendContextMock }); + expect(extendContextMock).toHaveBeenCalledTimes(0); // Not until we have opt-in + analyticsClient.optIn({ global: { enabled: false } }); + advance(10); + expect(extendContextMock).toHaveBeenCalledTimes(0); // Not until we have opt-in + }) + ); + + test( + 'Handles errors in the shipper', + fakeSchedulers((advance) => { + const optInMock = jest.fn().mockImplementation(() => { + throw new Error('Something went terribly wrong'); + }); + const extendContextMock = jest.fn().mockImplementation(() => { + throw new Error('Something went terribly wrong'); + }); + const shutdownMock = jest.fn().mockImplementation(() => { + throw new Error('Something went terribly wrong'); + }); + analyticsClient.registerShipper(MockedShipper, { + optInMock, + extendContextMock, + shutdownMock, + }); + expect(() => analyticsClient.optIn({ global: { enabled: true } })).not.toThrow(); + advance(10); + expect(optInMock).toHaveBeenCalledWith(true); + expect(extendContextMock).toHaveBeenCalledWith({}); // The initial context + expect(logger.warn).toHaveBeenCalledWith( + `Shipper "${MockedShipper.shipperName}" failed to extend the context`, + expect.any(Error) + ); + expect(() => analyticsClient.shutdown()).not.toThrow(); + expect(shutdownMock).toHaveBeenCalled(); + }) + ); }); describe('registerContextProvider', () => { @@ -719,6 +748,7 @@ describe.skip('AnalyticsClient', () => { expect(optInMock1).toHaveBeenCalledWith(false); // Using global and shipper-specific expect(optInMock2).toHaveBeenCalledWith(false); // Using only global }); + test('Catches error in the shipper.optIn method', () => { optInMock1.mockImplementation(() => { throw new Error('Something went terribly wrong'); @@ -819,86 +849,89 @@ describe.skip('AnalyticsClient', () => { ]); }); - test('Sends events from the internal queue when there are shippers and an opt-in response is true', async () => { - const telemetryCounterPromise = lastValueFrom( - analyticsClient.telemetryCounter$.pipe(take(3 + 2), toArray()) // Waiting for 3 enqueued + 2 batch-shipped events - ); - - // Send multiple events of 1 type to test the grouping logic as well - analyticsClient.reportEvent('event-type-a', { a_field: 'a' }); - analyticsClient.reportEvent('event-type-b', { b_field: 100 }); - analyticsClient.reportEvent('event-type-a', { a_field: 'b' }); - - // As proven in the previous test, the events are still enqueued. - // Let's register a shipper and opt-in to test the dequeue logic. - const reportEventsMock = jest.fn(); - analyticsClient.registerShipper(MockedShipper1, { reportEventsMock }); - analyticsClient.optIn({ global: { enabled: true } }); - await delay(10); - - expect(reportEventsMock).toHaveBeenCalledTimes(2); - expect(reportEventsMock).toHaveBeenNthCalledWith(1, [ - { - context: {}, - event_type: 'event-type-a', - properties: { a_field: 'a' }, - timestamp: expect.any(String), - }, - { - context: {}, - event_type: 'event-type-a', - properties: { a_field: 'b' }, - timestamp: expect.any(String), - }, - ]); - expect(reportEventsMock).toHaveBeenNthCalledWith(2, [ - { - context: {}, - event_type: 'event-type-b', - properties: { b_field: 100 }, - timestamp: expect.any(String), - }, - ]); - - // Expect 3 enqueued events, and 2 sent_to_shipper batched requests - await expect(telemetryCounterPromise).resolves.toEqual([ - { - type: 'enqueued', - source: 'client', - event_type: 'event-type-a', - code: 'enqueued', - count: 1, - }, - { - type: 'enqueued', - source: 'client', - event_type: 'event-type-b', - code: 'enqueued', - count: 1, - }, - { - type: 'enqueued', - source: 'client', - event_type: 'event-type-a', - code: 'enqueued', - count: 1, - }, - { - type: 'sent_to_shipper', - source: 'client', - event_type: 'event-type-a', - code: 'OK', - count: 2, - }, - { - type: 'sent_to_shipper', - source: 'client', - event_type: 'event-type-b', - code: 'OK', - count: 1, - }, - ]); - }); + test( + 'Sends events from the internal queue when there are shippers and an opt-in response is true', + fakeSchedulers(async (advance) => { + const telemetryCounterPromise = lastValueFrom( + analyticsClient.telemetryCounter$.pipe(take(3 + 2), toArray()) // Waiting for 3 enqueued + 2 batch-shipped events + ); + + // Send multiple events of 1 type to test the grouping logic as well + analyticsClient.reportEvent('event-type-a', { a_field: 'a' }); + analyticsClient.reportEvent('event-type-b', { b_field: 100 }); + analyticsClient.reportEvent('event-type-a', { a_field: 'b' }); + + // As proven in the previous test, the events are still enqueued. + // Let's register a shipper and opt-in to test the dequeue logic. + const reportEventsMock = jest.fn(); + analyticsClient.registerShipper(MockedShipper1, { reportEventsMock }); + analyticsClient.optIn({ global: { enabled: true } }); + advance(10); + + expect(reportEventsMock).toHaveBeenCalledTimes(2); + expect(reportEventsMock).toHaveBeenNthCalledWith(1, [ + { + context: {}, + event_type: 'event-type-a', + properties: { a_field: 'a' }, + timestamp: expect.any(String), + }, + { + context: {}, + event_type: 'event-type-a', + properties: { a_field: 'b' }, + timestamp: expect.any(String), + }, + ]); + expect(reportEventsMock).toHaveBeenNthCalledWith(2, [ + { + context: {}, + event_type: 'event-type-b', + properties: { b_field: 100 }, + timestamp: expect.any(String), + }, + ]); + + // Expect 3 enqueued events, and 2 sent_to_shipper batched requests + await expect(telemetryCounterPromise).resolves.toEqual([ + { + type: 'enqueued', + source: 'client', + event_type: 'event-type-a', + code: 'enqueued', + count: 1, + }, + { + type: 'enqueued', + source: 'client', + event_type: 'event-type-b', + code: 'enqueued', + count: 1, + }, + { + type: 'enqueued', + source: 'client', + event_type: 'event-type-a', + code: 'enqueued', + count: 1, + }, + { + type: 'sent_to_shipper', + source: 'client', + event_type: 'event-type-a', + code: 'OK', + count: 2, + }, + { + type: 'sent_to_shipper', + source: 'client', + event_type: 'event-type-b', + code: 'OK', + count: 1, + }, + ]); + }) + ); test('Discards events from the internal queue when there are shippers and an opt-in response is false', async () => { const telemetryCounterPromise = lastValueFrom( @@ -942,250 +975,259 @@ describe.skip('AnalyticsClient', () => { ]); }); - test('Discards only one type of the enqueued events based on event_type config', async () => { - const telemetryCounterPromise = lastValueFrom( - analyticsClient.telemetryCounter$.pipe(take(3 + 1), toArray()) // Waiting for 3 enqueued + 1 batch-shipped events - ); - - // Send multiple events of 1 type to test the grouping logic as well - analyticsClient.reportEvent('event-type-a', { a_field: 'a' }); - analyticsClient.reportEvent('event-type-b', { b_field: 100 }); - analyticsClient.reportEvent('event-type-a', { a_field: 'b' }); - - const reportEventsMock = jest.fn(); - analyticsClient.registerShipper(MockedShipper1, { reportEventsMock }); - analyticsClient.optIn({ - global: { enabled: true }, - event_types: { ['event-type-a']: { enabled: false } }, - }); - await delay(10); - - expect(reportEventsMock).toHaveBeenCalledTimes(1); - expect(reportEventsMock).toHaveBeenNthCalledWith(1, [ - { - context: {}, - event_type: 'event-type-b', - properties: { b_field: 100 }, - timestamp: expect.any(String), - }, - ]); - - // Expect 3 enqueued events, and 1 sent_to_shipper batched request - await expect(telemetryCounterPromise).resolves.toEqual([ - { - type: 'enqueued', - source: 'client', - event_type: 'event-type-a', - code: 'enqueued', - count: 1, - }, - { - type: 'enqueued', - source: 'client', - event_type: 'event-type-b', - code: 'enqueued', - count: 1, - }, - { - type: 'enqueued', - source: 'client', - event_type: 'event-type-a', - code: 'enqueued', - count: 1, - }, - { - type: 'sent_to_shipper', - source: 'client', - event_type: 'event-type-b', - code: 'OK', - count: 1, - }, - ]); - }); - - test('Discards the event at the shipper level (for a specific event)', async () => { - const telemetryCounterPromise = lastValueFrom( - analyticsClient.telemetryCounter$.pipe(take(3 + 2), toArray()) // Waiting for 3 enqueued + 2 batch-shipped events - ); - - // Send multiple events of 1 type to test the grouping logic as well - analyticsClient.reportEvent('event-type-a', { a_field: 'a' }); - analyticsClient.reportEvent('event-type-b', { b_field: 100 }); - analyticsClient.reportEvent('event-type-a', { a_field: 'b' }); - - // Register 2 shippers and set 1 of them as disabled for event-type-a - const reportEventsMock1 = jest.fn(); - const reportEventsMock2 = jest.fn(); - analyticsClient.registerShipper(MockedShipper1, { reportEventsMock: reportEventsMock1 }); - analyticsClient.registerShipper(MockedShipper2, { reportEventsMock: reportEventsMock2 }); - analyticsClient.optIn({ - global: { enabled: true }, - event_types: { - ['event-type-a']: { enabled: true, shippers: { [MockedShipper2.shipperName]: false } }, - }, - }); - await delay(10); - - expect(reportEventsMock1).toHaveBeenCalledTimes(2); - expect(reportEventsMock1).toHaveBeenNthCalledWith(1, [ - { - context: {}, - event_type: 'event-type-a', - properties: { a_field: 'a' }, - timestamp: expect.any(String), - }, - { - context: {}, - event_type: 'event-type-a', - properties: { a_field: 'b' }, - timestamp: expect.any(String), - }, - ]); - expect(reportEventsMock1).toHaveBeenNthCalledWith(2, [ - { - context: {}, - event_type: 'event-type-b', - properties: { b_field: 100 }, - timestamp: expect.any(String), - }, - ]); - expect(reportEventsMock2).toHaveBeenCalledTimes(1); - expect(reportEventsMock2).toHaveBeenNthCalledWith(1, [ - { - context: {}, - event_type: 'event-type-b', - properties: { b_field: 100 }, - timestamp: expect.any(String), - }, - ]); - - // Expect 3 enqueued events, and 2 sent_to_shipper batched requests - await expect(telemetryCounterPromise).resolves.toEqual([ - { - type: 'enqueued', - source: 'client', - event_type: 'event-type-a', - code: 'enqueued', - count: 1, - }, - { - type: 'enqueued', - source: 'client', - event_type: 'event-type-b', - code: 'enqueued', - count: 1, - }, - { - type: 'enqueued', - source: 'client', - event_type: 'event-type-a', - code: 'enqueued', - count: 1, - }, - { - type: 'sent_to_shipper', - source: 'client', - event_type: 'event-type-a', - code: 'OK', - count: 2, - }, - { - type: 'sent_to_shipper', - source: 'client', - event_type: 'event-type-b', - code: 'OK', - count: 1, - }, - ]); - }); - - test('Discards all the events at the shipper level (globally disabled)', async () => { - const telemetryCounterPromise = lastValueFrom( - analyticsClient.telemetryCounter$.pipe(take(3 + 2), toArray()) // Waiting for 3 enqueued + 2 batch-shipped events - ); - - // Send multiple events of 1 type to test the grouping logic as well - analyticsClient.reportEvent('event-type-a', { a_field: 'a' }); - analyticsClient.reportEvent('event-type-b', { b_field: 100 }); - analyticsClient.reportEvent('event-type-a', { a_field: 'b' }); - - // Register 2 shippers and set 1 of them as globally disabled - const reportEventsMock1 = jest.fn(); - const reportEventsMock2 = jest.fn(); - analyticsClient.registerShipper(MockedShipper1, { reportEventsMock: reportEventsMock1 }); - analyticsClient.registerShipper(MockedShipper2, { reportEventsMock: reportEventsMock2 }); - analyticsClient.optIn({ - global: { enabled: true, shippers: { [MockedShipper2.shipperName]: false } }, - event_types: { - ['event-type-a']: { enabled: true }, - }, - }); - await delay(10); - - expect(reportEventsMock1).toHaveBeenCalledTimes(2); - expect(reportEventsMock1).toHaveBeenNthCalledWith(1, [ - { - context: {}, - event_type: 'event-type-a', - properties: { a_field: 'a' }, - timestamp: expect.any(String), - }, - { - context: {}, - event_type: 'event-type-a', - properties: { a_field: 'b' }, - timestamp: expect.any(String), - }, - ]); - expect(reportEventsMock1).toHaveBeenNthCalledWith(2, [ - { - context: {}, - event_type: 'event-type-b', - properties: { b_field: 100 }, - timestamp: expect.any(String), - }, - ]); - expect(reportEventsMock2).toHaveBeenCalledTimes(0); - - // Expect 3 enqueued events, and 2 sent_to_shipper batched requests - await expect(telemetryCounterPromise).resolves.toEqual([ - { - type: 'enqueued', - source: 'client', - event_type: 'event-type-a', - code: 'enqueued', - count: 1, - }, - { - type: 'enqueued', - source: 'client', - event_type: 'event-type-b', - code: 'enqueued', - count: 1, - }, - { - type: 'enqueued', - source: 'client', - event_type: 'event-type-a', - code: 'enqueued', - count: 1, - }, - { - type: 'sent_to_shipper', - source: 'client', - event_type: 'event-type-a', - code: 'OK', - count: 2, - }, - { - type: 'sent_to_shipper', - source: 'client', - event_type: 'event-type-b', - code: 'OK', - count: 1, - }, - ]); - }); + test( + 'Discards only one type of the enqueued events based on event_type config', + fakeSchedulers(async (advance) => { + const telemetryCounterPromise = lastValueFrom( + analyticsClient.telemetryCounter$.pipe(take(3 + 1), toArray()) // Waiting for 3 enqueued + 1 batch-shipped events + ); + + // Send multiple events of 1 type to test the grouping logic as well + analyticsClient.reportEvent('event-type-a', { a_field: 'a' }); + analyticsClient.reportEvent('event-type-b', { b_field: 100 }); + analyticsClient.reportEvent('event-type-a', { a_field: 'b' }); + + const reportEventsMock = jest.fn(); + analyticsClient.registerShipper(MockedShipper1, { reportEventsMock }); + analyticsClient.optIn({ + global: { enabled: true }, + event_types: { ['event-type-a']: { enabled: false } }, + }); + advance(10); + + expect(reportEventsMock).toHaveBeenCalledTimes(1); + expect(reportEventsMock).toHaveBeenNthCalledWith(1, [ + { + context: {}, + event_type: 'event-type-b', + properties: { b_field: 100 }, + timestamp: expect.any(String), + }, + ]); + + // Expect 3 enqueued events, and 1 sent_to_shipper batched request + await expect(telemetryCounterPromise).resolves.toEqual([ + { + type: 'enqueued', + source: 'client', + event_type: 'event-type-a', + code: 'enqueued', + count: 1, + }, + { + type: 'enqueued', + source: 'client', + event_type: 'event-type-b', + code: 'enqueued', + count: 1, + }, + { + type: 'enqueued', + source: 'client', + event_type: 'event-type-a', + code: 'enqueued', + count: 1, + }, + { + type: 'sent_to_shipper', + source: 'client', + event_type: 'event-type-b', + code: 'OK', + count: 1, + }, + ]); + }) + ); + + test( + 'Discards the event at the shipper level (for a specific event)', + fakeSchedulers(async (advance) => { + const telemetryCounterPromise = lastValueFrom( + analyticsClient.telemetryCounter$.pipe(take(3 + 2), toArray()) // Waiting for 3 enqueued + 2 batch-shipped events + ); + + // Send multiple events of 1 type to test the grouping logic as well + analyticsClient.reportEvent('event-type-a', { a_field: 'a' }); + analyticsClient.reportEvent('event-type-b', { b_field: 100 }); + analyticsClient.reportEvent('event-type-a', { a_field: 'b' }); + + // Register 2 shippers and set 1 of them as disabled for event-type-a + const reportEventsMock1 = jest.fn(); + const reportEventsMock2 = jest.fn(); + analyticsClient.registerShipper(MockedShipper1, { reportEventsMock: reportEventsMock1 }); + analyticsClient.registerShipper(MockedShipper2, { reportEventsMock: reportEventsMock2 }); + analyticsClient.optIn({ + global: { enabled: true }, + event_types: { + ['event-type-a']: { enabled: true, shippers: { [MockedShipper2.shipperName]: false } }, + }, + }); + advance(10); + + expect(reportEventsMock1).toHaveBeenCalledTimes(2); + expect(reportEventsMock1).toHaveBeenNthCalledWith(1, [ + { + context: {}, + event_type: 'event-type-a', + properties: { a_field: 'a' }, + timestamp: expect.any(String), + }, + { + context: {}, + event_type: 'event-type-a', + properties: { a_field: 'b' }, + timestamp: expect.any(String), + }, + ]); + expect(reportEventsMock1).toHaveBeenNthCalledWith(2, [ + { + context: {}, + event_type: 'event-type-b', + properties: { b_field: 100 }, + timestamp: expect.any(String), + }, + ]); + expect(reportEventsMock2).toHaveBeenCalledTimes(1); + expect(reportEventsMock2).toHaveBeenNthCalledWith(1, [ + { + context: {}, + event_type: 'event-type-b', + properties: { b_field: 100 }, + timestamp: expect.any(String), + }, + ]); + + // Expect 3 enqueued events, and 2 sent_to_shipper batched requests + await expect(telemetryCounterPromise).resolves.toEqual([ + { + type: 'enqueued', + source: 'client', + event_type: 'event-type-a', + code: 'enqueued', + count: 1, + }, + { + type: 'enqueued', + source: 'client', + event_type: 'event-type-b', + code: 'enqueued', + count: 1, + }, + { + type: 'enqueued', + source: 'client', + event_type: 'event-type-a', + code: 'enqueued', + count: 1, + }, + { + type: 'sent_to_shipper', + source: 'client', + event_type: 'event-type-a', + code: 'OK', + count: 2, + }, + { + type: 'sent_to_shipper', + source: 'client', + event_type: 'event-type-b', + code: 'OK', + count: 1, + }, + ]); + }) + ); + + test( + 'Discards all the events at the shipper level (globally disabled)', + fakeSchedulers(async (advance) => { + const telemetryCounterPromise = lastValueFrom( + analyticsClient.telemetryCounter$.pipe(take(3 + 2), toArray()) // Waiting for 3 enqueued + 2 batch-shipped events + ); + + // Send multiple events of 1 type to test the grouping logic as well + analyticsClient.reportEvent('event-type-a', { a_field: 'a' }); + analyticsClient.reportEvent('event-type-b', { b_field: 100 }); + analyticsClient.reportEvent('event-type-a', { a_field: 'b' }); + + // Register 2 shippers and set 1 of them as globally disabled + const reportEventsMock1 = jest.fn(); + const reportEventsMock2 = jest.fn(); + analyticsClient.registerShipper(MockedShipper1, { reportEventsMock: reportEventsMock1 }); + analyticsClient.registerShipper(MockedShipper2, { reportEventsMock: reportEventsMock2 }); + analyticsClient.optIn({ + global: { enabled: true, shippers: { [MockedShipper2.shipperName]: false } }, + event_types: { + ['event-type-a']: { enabled: true }, + }, + }); + advance(10); + + expect(reportEventsMock1).toHaveBeenCalledTimes(2); + expect(reportEventsMock1).toHaveBeenNthCalledWith(1, [ + { + context: {}, + event_type: 'event-type-a', + properties: { a_field: 'a' }, + timestamp: expect.any(String), + }, + { + context: {}, + event_type: 'event-type-a', + properties: { a_field: 'b' }, + timestamp: expect.any(String), + }, + ]); + expect(reportEventsMock1).toHaveBeenNthCalledWith(2, [ + { + context: {}, + event_type: 'event-type-b', + properties: { b_field: 100 }, + timestamp: expect.any(String), + }, + ]); + expect(reportEventsMock2).toHaveBeenCalledTimes(0); + + // Expect 3 enqueued events, and 2 sent_to_shipper batched requests + await expect(telemetryCounterPromise).resolves.toEqual([ + { + type: 'enqueued', + source: 'client', + event_type: 'event-type-a', + code: 'enqueued', + count: 1, + }, + { + type: 'enqueued', + source: 'client', + event_type: 'event-type-b', + code: 'enqueued', + count: 1, + }, + { + type: 'enqueued', + source: 'client', + event_type: 'event-type-a', + code: 'enqueued', + count: 1, + }, + { + type: 'sent_to_shipper', + source: 'client', + event_type: 'event-type-a', + code: 'OK', + count: 2, + }, + { + type: 'sent_to_shipper', + source: 'client', + event_type: 'event-type-b', + code: 'OK', + count: 1, + }, + ]); + }) + ); test('Discards incoming events when opt-in response is false', async () => { // Set OptIn and shipper first to test the "once-set up" scenario diff --git a/packages/analytics/client/src/analytics_client/analytics_client.ts b/packages/analytics/client/src/analytics_client/analytics_client.ts index 1c7f261aaf4b4..e38f975fee5a9 100644 --- a/packages/analytics/client/src/analytics_client/analytics_client.ts +++ b/packages/analytics/client/src/analytics_client/analytics_client.ts @@ -208,6 +208,21 @@ export class AnalyticsClient implements IAnalyticsClient { this.shipperRegistered$.next(); }; + public shutdown = () => { + this.shippersRegistry.allShippers.forEach((shipper, shipperName) => { + try { + shipper.shutdown(); + } catch (err) { + this.initContext.logger.warn(`Failed to shutdown shipper "${shipperName}"`, err); + } + }); + this.internalEventQueue$.complete(); + this.internalTelemetryCounter$.complete(); + this.shipperRegistered$.complete(); + this.optInConfig$.complete(); + this.context$.complete(); + }; + /** * Forwards the `events` to the registered shippers, bearing in mind if the shipper is opted-in for that eventType. * @param eventType The event type's name diff --git a/packages/analytics/client/src/analytics_client/mocks.ts b/packages/analytics/client/src/analytics_client/mocks.ts index da707178a756e..221ca0ff3872f 100644 --- a/packages/analytics/client/src/analytics_client/mocks.ts +++ b/packages/analytics/client/src/analytics_client/mocks.ts @@ -18,6 +18,7 @@ function createMockedAnalyticsClient(): jest.Mocked { removeContextProvider: jest.fn(), registerShipper: jest.fn(), telemetryCounter$: new Subject(), + shutdown: jest.fn(), }; } diff --git a/packages/analytics/client/src/analytics_client/types.ts b/packages/analytics/client/src/analytics_client/types.ts index 782d6b16fa594..88b60dc100e89 100644 --- a/packages/analytics/client/src/analytics_client/types.ts +++ b/packages/analytics/client/src/analytics_client/types.ts @@ -176,7 +176,7 @@ export interface IAnalyticsClient { ) => void; /** * Registers the event type that will be emitted via the reportEvent API. - * @param eventTypeOps + * @param eventTypeOps The definition of the event type {@link EventTypeOpts}. */ registerEventType: (eventTypeOps: EventTypeOpts) => void; @@ -211,4 +211,8 @@ export interface IAnalyticsClient { * Observable to emit the stats of the processed events. */ readonly telemetryCounter$: Observable; + /** + * Stops the client. + */ + shutdown: () => void; } diff --git a/packages/analytics/client/src/events/types.ts b/packages/analytics/client/src/events/types.ts index bc193a2788db1..6d31f88621364 100644 --- a/packages/analytics/client/src/events/types.ts +++ b/packages/analytics/client/src/events/types.ts @@ -12,6 +12,18 @@ import type { ShipperName } from '../analytics_client'; * Definition of the context that can be appended to the events through the {@link IAnalyticsClient.registerContextProvider}. */ export interface EventContext { + /** + * The UUID of the cluster + */ + cluster_uuid?: string; + /** + * The name of the cluster. + */ + cluster_name?: string; + /** + * The license ID. + */ + license_id?: string; /** * The unique user ID. */ @@ -36,7 +48,10 @@ export interface EventContext { * The current entity ID (dashboard ID, visualization ID, etc.). */ entityId?: string; - // TODO: Extend with known keys + + /** + * Additional keys are allowed. + */ [key: string]: unknown; } diff --git a/packages/analytics/client/src/schema/types.test.ts b/packages/analytics/client/src/schema/types.test.ts index 3ed25e46d6dc9..05eccf2bb19c7 100644 --- a/packages/analytics/client/src/schema/types.test.ts +++ b/packages/analytics/client/src/schema/types.test.ts @@ -421,6 +421,48 @@ describe('schema types', () => { }; expect(valueType).not.toBeUndefined(); // <-- Only to stop the var-not-used complain }); + + test('it should expect support readonly arrays', () => { + let valueType: SchemaValue> = { + type: 'array', + items: { + properties: { + a_value: { + type: 'keyword', + _meta: { + description: 'Some description', + }, + }, + }, + }, + }; + + valueType = { + type: 'array', + items: { + properties: { + a_value: { + type: 'keyword', + _meta: { + description: 'Some description', + optional: false, + }, + }, + }, + _meta: { + description: 'Description at the object level', + }, + }, + }; + + // @ts-expect-error because it's missing the items definition + valueType = { type: 'array' }; + // @ts-expect-error because it's missing the items definition + valueType = { type: 'array', items: {} }; + // @ts-expect-error because it's missing the items' properties definition + valueType = { type: 'array', items: { properties: {} } }; + expect(valueType).not.toBeUndefined(); // <-- Only to stop the var-not-used complain + }); }); }); diff --git a/packages/analytics/client/src/schema/types.ts b/packages/analytics/client/src/schema/types.ts index 8bac1ceaad620..5043c46e73fd4 100644 --- a/packages/analytics/client/src/schema/types.ts +++ b/packages/analytics/client/src/schema/types.ts @@ -64,7 +64,7 @@ export type SchemaValue = ? // If the Value is unknown (TS can't infer the type), allow any type of schema SchemaArray | SchemaObject | SchemaChildValue : // Otherwise, try to infer the type and enforce the schema - NonNullable extends Array + NonNullable extends Array | ReadonlyArray ? SchemaArray : NonNullable extends object ? SchemaObject diff --git a/packages/analytics/client/src/shippers/mocks.ts b/packages/analytics/client/src/shippers/mocks.ts index 6f80d91675c97..4660ae9d27e72 100644 --- a/packages/analytics/client/src/shippers/mocks.ts +++ b/packages/analytics/client/src/shippers/mocks.ts @@ -23,6 +23,7 @@ class MockedShipper implements IShipper { public reportEvents = jest.fn(); public extendContext = jest.fn(); public telemetryCounter$ = new Subject(); + public shutdown = jest.fn(); } export const shippersMock = { diff --git a/packages/analytics/client/src/shippers/types.ts b/packages/analytics/client/src/shippers/types.ts index 945ea08dae58a..67fe2c54bd77e 100644 --- a/packages/analytics/client/src/shippers/types.ts +++ b/packages/analytics/client/src/shippers/types.ts @@ -25,11 +25,15 @@ export interface IShipper { optIn: (isOptedIn: boolean) => void; /** * Perform any necessary calls to the persisting/analytics solution to set the event's context. - * @param newContext + * @param newContext The full new context to set {@link EventContext} */ extendContext?: (newContext: EventContext) => void; /** * Observable to emit the stats of the processed events. */ telemetryCounter$?: Observable; + /** + * Shutdown the shipper. + */ + shutdown: () => void; } diff --git a/packages/analytics/shippers/README.md b/packages/analytics/shippers/README.md index 8fc04b9f9ecbc..5ab85d08ff2ca 100644 --- a/packages/analytics/shippers/README.md +++ b/packages/analytics/shippers/README.md @@ -3,3 +3,5 @@ This directory holds the implementation of the _built-in_ shippers provided by the Analytics client. At the moment, the shippers are: * [FullStory](./fullstory/README.md) +* [Elastic V3 (Browser shipper)](./elastic_v3/browser/README.md) +* [Elastic V3 (Server-side shipper)](./elastic_v3/server/README.md) diff --git a/packages/analytics/shippers/elastic_v3/browser/BUILD.bazel b/packages/analytics/shippers/elastic_v3/browser/BUILD.bazel new file mode 100644 index 0000000000000..63332da454feb --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/browser/BUILD.bazel @@ -0,0 +1,128 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "browser" +PKG_REQUIRE_NAME = "@kbn/analytics-shippers-elastic-v3-browser" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//rxjs", + "//packages/analytics/client", + "//packages/analytics/shippers/elastic_v3/common", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//rxjs", + "//packages/analytics/client:npm_module_types", + "//packages/analytics/shippers/elastic_v3/common:npm_module_types", + "//packages/kbn-logging-mocks:npm_module_types", +] + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/analytics/shippers/elastic_v3/browser/README.md b/packages/analytics/shippers/elastic_v3/browser/README.md new file mode 100644 index 0000000000000..fcdd5e8a3ca2e --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/browser/README.md @@ -0,0 +1,25 @@ +# @kbn/analytics-shippers-elastic-v3-browser + +UI-side implementation of the Elastic V3 shipper for the `@kbn/analytics-client`. + +## How to use it + +This module is intended to be used **on the browser only**. Due to the nature of the UI events, they are usually more scattered in time, and we can assume a much lower load than the server. For that reason, it doesn't apply the necessary backpressure mechanisms to prevent the server from getting overloaded with too many events neither identifies if the server sits behind a firewall to discard any incoming events. Refer to `@kbn/analytics-shippers-elastic-v3-server` for the server-side implementation. + +```typescript +import { ElasticV3BrowserShipper } from "@kbn/analytics-shippers-elastic-v3-browser"; + +analytics.registerShipper(ElasticV3BrowserShipper, { channelName: 'myChannel', version: '1.0.0' }); +``` + +## Configuration + +| Name | Description | +|:-------------:|:-------------------------------------------------------------------------------------------| +| `channelName` | The name of the channel to send the events. | +| `version` | The version of the application generating the events. | +| `debug` | When `true`, it logs the responses from the remote Telemetry Service. Defaults to `false`. | + +## Transmission protocol + +This shipper sends the events to the Elastic Internal Telemetry Service. The incoming events are buffered for up to 1 second to attempt to send them in a single request. diff --git a/packages/analytics/shippers/elastic_v3/browser/jest.config.js b/packages/analytics/shippers/elastic_v3/browser/jest.config.js new file mode 100644 index 0000000000000..20067863aad97 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/browser/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../../', + roots: ['/packages/analytics/shippers/elastic_v3/browser'], +}; diff --git a/packages/analytics/shippers/elastic_v3/browser/package.json b/packages/analytics/shippers/elastic_v3/browser/package.json new file mode 100644 index 0000000000000..418887000521a --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/browser/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/analytics-shippers-elastic-v3-browser", + "private": true, + "version": "1.0.0", + "browser": "./target_web/index.js", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/analytics/shippers/elastic_v3/browser/src/browser_shipper.test.ts b/packages/analytics/shippers/elastic_v3/browser/src/browser_shipper.test.ts new file mode 100644 index 0000000000000..6fbe8fc166586 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/browser/src/browser_shipper.test.ts @@ -0,0 +1,291 @@ +/* + * Copyright 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 { loggerMock } from '@kbn/logging-mocks'; +import type { AnalyticsClientInitContext, Event } from '@kbn/analytics-client'; +import { fakeSchedulers } from 'rxjs-marbles/jest'; +import { firstValueFrom } from 'rxjs'; +import { ElasticV3BrowserShipper } from './browser_shipper'; + +describe('ElasticV3BrowserShipper', () => { + const events: Event[] = [ + { + timestamp: '2020-01-01T00:00:00.000Z', + event_type: 'test-event-type', + context: {}, + properties: {}, + }, + ]; + + const initContext: AnalyticsClientInitContext = { + sendTo: 'staging', + isDev: true, + logger: loggerMock.create(), + }; + + let shipper: ElasticV3BrowserShipper; + + let fetchMock: jest.Mock; + + beforeEach(() => { + jest.useFakeTimers(); + + fetchMock = jest.fn().mockResolvedValue({ + status: 200, + ok: true, + text: () => Promise.resolve('{"status": "ok"}'), + }); + + Object.defineProperty(global, 'fetch', { + value: fetchMock, + writable: true, + }); + + shipper = new ElasticV3BrowserShipper( + { version: '1.2.3', channelName: 'test-channel', debug: true }, + initContext + ); + }); + + afterEach(() => { + shipper.shutdown(); + jest.useRealTimers(); + }); + + test("custom sendTo overrides Analytics client's", () => { + const prodShipper = new ElasticV3BrowserShipper( + { version: '1.2.3', channelName: 'test-channel', debug: true, sendTo: 'production' }, + initContext + ); + + // eslint-disable-next-line dot-notation + expect(prodShipper['url']).not.toEqual(shipper['url']); + }); + + test('set optIn should update the isOptedIn$ observable', () => { + // eslint-disable-next-line dot-notation + const internalOptIn$ = shipper['isOptedIn$']; + + // Initially undefined + expect(internalOptIn$.value).toBeUndefined(); + + shipper.optIn(true); + expect(internalOptIn$.value).toBe(true); + + shipper.optIn(false); + expect(internalOptIn$.value).toBe(false); + }); + + test('set extendContext should store local values: clusterUuid and licenseId', () => { + // eslint-disable-next-line dot-notation + const getInternalClusterUuid = () => shipper['clusterUuid']; + // eslint-disable-next-line dot-notation + const getInternalLicenseId = () => shipper['licenseId']; + + // Initial values + expect(getInternalClusterUuid()).toBe('UNKNOWN'); + expect(getInternalLicenseId()).toBeUndefined(); + + shipper.extendContext({ cluster_uuid: 'test-cluster-uuid' }); + expect(getInternalClusterUuid()).toBe('test-cluster-uuid'); + expect(getInternalLicenseId()).toBeUndefined(); + + shipper.extendContext({ license_id: 'test-license-id' }); + expect(getInternalClusterUuid()).toBe('test-cluster-uuid'); + expect(getInternalLicenseId()).toBe('test-license-id'); + + shipper.extendContext({ cluster_uuid: 'test-cluster-uuid-2', license_id: 'test-license-id-2' }); + expect(getInternalClusterUuid()).toBe('test-cluster-uuid-2'); + expect(getInternalLicenseId()).toBe('test-license-id-2'); + }); + + test('calls to reportEvents do not call `fetch` straight away (buffer of 1s)', () => { + shipper.reportEvents(events); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + test( + 'calls to reportEvents do not call `fetch` after 1s because no optIn value is set yet', + fakeSchedulers((advance) => { + shipper.reportEvents(events); + advance(1000); + expect(fetchMock).not.toHaveBeenCalled(); + }) + ); + + test( + 'calls to reportEvents call `fetch` after 1s when optIn value is set to true', + fakeSchedulers(async (advance) => { + shipper.reportEvents(events); + shipper.optIn(true); + const counter = firstValueFrom(shipper.telemetryCounter$); + advance(1000); + expect(fetchMock).toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { + body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', + headers: { + 'content-type': 'application/x-njson', + 'x-elastic-cluster-id': 'UNKNOWN', + 'x-elastic-stack-version': '1.2.3', + }, + keepalive: true, + method: 'POST', + query: { debug: true }, + } + ); + await expect(counter).resolves.toMatchInlineSnapshot(` + Object { + "code": "200", + "count": 1, + "event_type": "test-event-type", + "source": "elastic_v3_browser", + "type": "succeeded", + } + `); + }) + ); + + test( + 'calls to reportEvents do not call `fetch` after 1s when optIn value is set to false', + fakeSchedulers((advance) => { + shipper.reportEvents(events); + shipper.optIn(false); + advance(1000); + expect(fetchMock).not.toHaveBeenCalled(); + }) + ); + + test('calls to reportEvents call `fetch` when shutting down if optIn value is set to true', async () => { + shipper.reportEvents(events); + shipper.optIn(true); + const counter = firstValueFrom(shipper.telemetryCounter$); + shipper.shutdown(); + expect(fetchMock).toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { + body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', + headers: { + 'content-type': 'application/x-njson', + 'x-elastic-cluster-id': 'UNKNOWN', + 'x-elastic-stack-version': '1.2.3', + }, + keepalive: true, + method: 'POST', + query: { debug: true }, + } + ); + await expect(counter).resolves.toMatchInlineSnapshot(` + Object { + "code": "200", + "count": 1, + "event_type": "test-event-type", + "source": "elastic_v3_browser", + "type": "succeeded", + } + `); + }); + + test( + 'does not add the query.debug: true property to the request if the shipper is not set with the debug flag', + fakeSchedulers((advance) => { + shipper = new ElasticV3BrowserShipper( + { version: '1.2.3', channelName: 'test-channel' }, + initContext + ); + shipper.reportEvents(events); + shipper.optIn(true); + advance(1000); + expect(fetchMock).toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { + body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', + headers: { + 'content-type': 'application/x-njson', + 'x-elastic-cluster-id': 'UNKNOWN', + 'x-elastic-stack-version': '1.2.3', + }, + keepalive: true, + method: 'POST', + } + ); + }) + ); + + test( + 'handles when the fetch request fails', + fakeSchedulers(async (advance) => { + fetchMock.mockRejectedValueOnce(new Error('Failed to fetch')); + shipper.reportEvents(events); + shipper.optIn(true); + const counter = firstValueFrom(shipper.telemetryCounter$); + advance(1000); + expect(fetchMock).toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { + body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', + headers: { + 'content-type': 'application/x-njson', + 'x-elastic-cluster-id': 'UNKNOWN', + 'x-elastic-stack-version': '1.2.3', + }, + keepalive: true, + method: 'POST', + query: { debug: true }, + } + ); + await expect(counter).resolves.toMatchInlineSnapshot(` + Object { + "code": "Failed to fetch", + "count": 1, + "event_type": "test-event-type", + "source": "elastic_v3_browser", + "type": "failed", + } + `); + }) + ); + + test( + 'handles when the fetch request fails (request completes but not OK response)', + fakeSchedulers(async (advance) => { + fetchMock.mockResolvedValue({ + ok: false, + status: 400, + text: () => Promise.resolve('{"status": "not ok"}'), + }); + shipper.reportEvents(events); + shipper.optIn(true); + const counter = firstValueFrom(shipper.telemetryCounter$); + advance(1000); + expect(fetchMock).toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { + body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', + headers: { + 'content-type': 'application/x-njson', + 'x-elastic-cluster-id': 'UNKNOWN', + 'x-elastic-stack-version': '1.2.3', + }, + keepalive: true, + method: 'POST', + query: { debug: true }, + } + ); + await expect(counter).resolves.toMatchInlineSnapshot(` + Object { + "code": "400", + "count": 1, + "event_type": "test-event-type", + "source": "elastic_v3_browser", + "type": "failed", + } + `); + }) + ); +}); diff --git a/packages/analytics/shippers/elastic_v3/browser/src/browser_shipper.ts b/packages/analytics/shippers/elastic_v3/browser/src/browser_shipper.ts new file mode 100644 index 0000000000000..5607d3d79a13d --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/browser/src/browser_shipper.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { BehaviorSubject, interval, Subject, bufferWhen, concatMap, filter, skipWhile } from 'rxjs'; +import type { + AnalyticsClientInitContext, + Event, + EventContext, + IShipper, + TelemetryCounter, +} from '@kbn/analytics-client'; +import { ElasticV3ShipperOptions, ErrorWithCode } from '@kbn/analytics-shippers-elastic-v3-common'; +import { + buildHeaders, + buildUrl, + createTelemetryCounterHelper, + eventsToNDJSON, +} from '@kbn/analytics-shippers-elastic-v3-common'; + +export class ElasticV3BrowserShipper implements IShipper { + public static shipperName = 'elastic_v3_browser'; + public readonly telemetryCounter$ = new Subject(); + + private readonly reportTelemetryCounters = createTelemetryCounterHelper( + this.telemetryCounter$, + ElasticV3BrowserShipper.shipperName + ); + private readonly url: string; + + private readonly internalQueue$ = new Subject(); + + private readonly isOptedIn$ = new BehaviorSubject(undefined); + private clusterUuid: string = 'UNKNOWN'; + private licenseId: string | undefined; + + constructor( + private readonly options: ElasticV3ShipperOptions, + private readonly initContext: AnalyticsClientInitContext + ) { + this.setUpInternalQueueSubscriber(); + this.url = buildUrl({ + sendTo: options.sendTo ?? initContext.sendTo, + channelName: options.channelName, + }); + } + + public extendContext(newContext: EventContext) { + if (newContext.cluster_uuid) { + this.clusterUuid = newContext.cluster_uuid; + } + if (newContext.license_id) { + this.licenseId = newContext.license_id; + } + } + + public optIn(isOptedIn: boolean) { + this.isOptedIn$.next(isOptedIn); + } + + public reportEvents(events: Event[]) { + events.forEach((event) => { + this.internalQueue$.next(event); + }); + } + + public shutdown() { + this.internalQueue$.complete(); // NOTE: When completing the observable, the buffer logic does not wait and releases any buffered events. + } + + private setUpInternalQueueSubscriber() { + this.internalQueue$ + .pipe( + // Buffer events for 1 second or until we have an optIn value + bufferWhen(() => interval(1000).pipe(skipWhile(() => this.isOptedIn$.value === undefined))), + // Discard any events if we are not opted in + skipWhile(() => this.isOptedIn$.value === false), + // Skip empty buffers + filter((events) => events.length > 0), + // Send events + concatMap(async (events) => this.sendEvents(events)) + ) + .subscribe(); + } + + private async sendEvents(events: Event[]) { + try { + const code = await this.makeRequest(events); + this.reportTelemetryCounters(events, { code }); + } catch (error) { + this.reportTelemetryCounters(events, { code: error.code, error }); + } + } + + private async makeRequest(events: Event[]): Promise { + const { status, text, ok } = await fetch(this.url, { + method: 'POST', + body: eventsToNDJSON(events), + headers: buildHeaders(this.clusterUuid, this.options.version, this.licenseId), + ...(this.options.debug && { query: { debug: true } }), + // Allow the request to outlive the page in case the tab is closed + keepalive: true, + }); + + if (this.options.debug) { + this.initContext.logger.debug( + `[${ElasticV3BrowserShipper.shipperName}]: ${status} - ${await text()}` + ); + } + + if (!ok) { + throw new ErrorWithCode(`${status} - ${await text()}`, `${status}`); + } + + return `${status}`; + } +} diff --git a/packages/analytics/shippers/elastic_v3/browser/src/index.ts b/packages/analytics/shippers/elastic_v3/browser/src/index.ts new file mode 100644 index 0000000000000..9eb6668994037 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/browser/src/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { ElasticV3ShipperOptions } from '@kbn/analytics-shippers-elastic-v3-common'; +export { ElasticV3BrowserShipper } from './browser_shipper'; diff --git a/packages/analytics/shippers/elastic_v3/browser/tsconfig.json b/packages/analytics/shippers/elastic_v3/browser/tsconfig.json new file mode 100644 index 0000000000000..b81a9a0217634 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/browser/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/analytics/shippers/elastic_v3/common/BUILD.bazel b/packages/analytics/shippers/elastic_v3/common/BUILD.bazel new file mode 100644 index 0000000000000..a09ee9019f93d --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/BUILD.bazel @@ -0,0 +1,125 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "common" +PKG_REQUIRE_NAME = "@kbn/analytics-shippers-elastic-v3-common" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//rxjs", + "//packages/analytics/client", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//rxjs", + "//packages/analytics/client:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/analytics/shippers/elastic_v3/common/README.md b/packages/analytics/shippers/elastic_v3/common/README.md new file mode 100644 index 0000000000000..6684027b1ed7f --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/README.md @@ -0,0 +1,10 @@ +# @kbn/analytics-shippers-elastic-v3-common + +This package holds the common code for the Elastic V3 shippers: + +- Types defining the Shipper configuration `ElasticV3ShipperOptions` +- `buildUrl` utility helps decide which URL to use depending on whether the shipper is configured to send to production or staging. +- `eventsToNdjson` utility converts any array of events to NDJSON format. +- `reportTelemetryCounters` helps with building the TelemetryCounter to emit after processing an event. + +It should be considered an internal package and should not be used other than by the shipper implementations: `@kbn/analytics-shippers-elastic-v3-browser` and `@kbn/analytics-shippers-elastic-v3-server` diff --git a/packages/analytics/shippers/elastic_v3/common/jest.config.js b/packages/analytics/shippers/elastic_v3/common/jest.config.js new file mode 100644 index 0000000000000..1336211347fb7 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../../', + roots: ['/packages/analytics/shippers/elastic_v3/common'], +}; diff --git a/packages/analytics/shippers/elastic_v3/common/package.json b/packages/analytics/shippers/elastic_v3/common/package.json new file mode 100644 index 0000000000000..ce8825e6bdc1f --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/analytics-shippers-elastic-v3-common", + "private": true, + "version": "1.0.0", + "browser": "./target_web/index.js", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/analytics/shippers/elastic_v3/common/src/build_headers.test.ts b/packages/analytics/shippers/elastic_v3/common/src/build_headers.test.ts new file mode 100644 index 0000000000000..468824916ec48 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/src/build_headers.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 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 { buildHeaders } from './build_headers'; + +describe('buildHeaders', () => { + test('builds the headers as expected in the V3 endpoints', () => { + expect(buildHeaders('test-cluster', '1.2.3', 'test-license')).toMatchInlineSnapshot(` + Object { + "content-type": "application/x-njson", + "x-elastic-cluster-id": "test-cluster", + "x-elastic-license-id": "test-license", + "x-elastic-stack-version": "1.2.3", + } + `); + }); + + test('if license is not provided, it skips the license header', () => { + expect(buildHeaders('test-cluster', '1.2.3')).toMatchInlineSnapshot(` + Object { + "content-type": "application/x-njson", + "x-elastic-cluster-id": "test-cluster", + "x-elastic-stack-version": "1.2.3", + } + `); + }); +}); diff --git a/packages/analytics/shippers/elastic_v3/common/src/build_headers.ts b/packages/analytics/shippers/elastic_v3/common/src/build_headers.ts new file mode 100644 index 0000000000000..43126cf9d5629 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/src/build_headers.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export function buildHeaders(clusterUuid: string, version: string, licenseId?: string) { + return { + 'content-type': 'application/x-njson', + 'x-elastic-cluster-id': clusterUuid, + 'x-elastic-stack-version': version, + ...(licenseId && { 'x-elastic-license-id': licenseId }), + }; +} diff --git a/packages/analytics/shippers/elastic_v3/common/src/build_url.test.ts b/packages/analytics/shippers/elastic_v3/common/src/build_url.test.ts new file mode 100644 index 0000000000000..171491757ea56 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/src/build_url.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { buildUrl } from './build_url'; + +describe('buildUrl', () => { + test('returns production URL', () => { + expect(buildUrl({ sendTo: 'production', channelName: 'test-channel' })).toBe( + 'https://telemetry.elastic.co/v3/send/test-channel' + ); + }); + + test('returns staging URL', () => { + expect(buildUrl({ sendTo: 'staging', channelName: 'test-channel' })).toBe( + 'https://telemetry-staging.elastic.co/v3/send/test-channel' + ); + }); +}); diff --git a/packages/analytics/shippers/elastic_v3/common/src/build_url.ts b/packages/analytics/shippers/elastic_v3/common/src/build_url.ts new file mode 100644 index 0000000000000..f8d88b6804d08 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/src/build_url.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 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. + */ + +/** + * Builds the URL for the V3 API. + * @param sendTo Whether to send it to production or staging. + * @param channelName The name of the channel to send the data to. + */ +export function buildUrl({ + sendTo, + channelName, +}: { + sendTo: 'production' | 'staging'; + channelName: string; +}): string { + const baseUrl = + sendTo === 'production' + ? 'https://telemetry.elastic.co' + : 'https://telemetry-staging.elastic.co'; + return `${baseUrl}/v3/send/${channelName}`; +} diff --git a/packages/analytics/shippers/elastic_v3/common/src/error_with_code.test.ts b/packages/analytics/shippers/elastic_v3/common/src/error_with_code.test.ts new file mode 100644 index 0000000000000..9be5d5bdc762c --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/src/error_with_code.test.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ErrorWithCode } from './error_with_code'; + +describe('ErrorWithCode', () => { + const error = new ErrorWithCode('test', 'test_code'); + test('message and code properties are publicly accessible', () => { + expect(error.message).toBe('test'); + expect(error.code).toBe('test_code'); + }); + test('extends error', () => { + expect(error).toBeInstanceOf(Error); + }); +}); diff --git a/packages/analytics/shippers/elastic_v3/common/src/error_with_code.ts b/packages/analytics/shippers/elastic_v3/common/src/error_with_code.ts new file mode 100644 index 0000000000000..09346575a2df1 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/src/error_with_code.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export class ErrorWithCode extends Error { + constructor(message: string, public readonly code: string) { + super(message); + } +} diff --git a/packages/analytics/shippers/elastic_v3/common/src/events_to_ndjson.test.ts b/packages/analytics/shippers/elastic_v3/common/src/events_to_ndjson.test.ts new file mode 100644 index 0000000000000..4033bcb862967 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/src/events_to_ndjson.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Event } from '@kbn/analytics-client'; +import { eventsToNDJSON } from './events_to_ndjson'; + +describe('eventsToNDJSON', () => { + test('works with one event', () => { + const event: Event = { + timestamp: '2020-01-01T00:00:00.000Z', + event_type: 'event_type', + context: {}, + properties: {}, + }; + + // Mind the extra line at the bottom + expect(eventsToNDJSON([event])).toMatchInlineSnapshot(` + "{\\"timestamp\\":\\"2020-01-01T00:00:00.000Z\\",\\"event_type\\":\\"event_type\\",\\"context\\":{},\\"properties\\":{}} + " + `); + }); + + test('works with many events', () => { + const events: Event[] = [ + { + timestamp: '2020-01-01T00:00:00.000Z', + event_type: 'event_type', + context: {}, + properties: {}, + }, + { + timestamp: '2020-01-02T00:00:00.000Z', + event_type: 'event_type', + context: {}, + properties: {}, + }, + ]; + + expect(eventsToNDJSON(events)).toMatchInlineSnapshot(` + "{\\"timestamp\\":\\"2020-01-01T00:00:00.000Z\\",\\"event_type\\":\\"event_type\\",\\"context\\":{},\\"properties\\":{}} + {\\"timestamp\\":\\"2020-01-02T00:00:00.000Z\\",\\"event_type\\":\\"event_type\\",\\"context\\":{},\\"properties\\":{}} + " + `); + }); +}); diff --git a/packages/analytics/shippers/elastic_v3/common/src/events_to_ndjson.ts b/packages/analytics/shippers/elastic_v3/common/src/events_to_ndjson.ts new file mode 100644 index 0000000000000..af768770a0d5f --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/src/events_to_ndjson.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Event } from '@kbn/analytics-client'; + +export function eventsToNDJSON(events: Event[]): string { + return `${events.map((event) => JSON.stringify(event)).join('\n')}\n`; +} diff --git a/packages/analytics/shippers/elastic_v3/common/src/index.ts b/packages/analytics/shippers/elastic_v3/common/src/index.ts new file mode 100644 index 0000000000000..043be5c33c6fe --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/src/index.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 { buildHeaders } from './build_headers'; +export { buildUrl } from './build_url'; +export { ErrorWithCode } from './error_with_code'; +export { eventsToNDJSON } from './events_to_ndjson'; +export { createTelemetryCounterHelper } from './report_telemetry_counters'; +export type { ElasticV3ShipperOptions } from './types'; diff --git a/packages/analytics/shippers/elastic_v3/common/src/report_telemetry_counters.test.ts b/packages/analytics/shippers/elastic_v3/common/src/report_telemetry_counters.test.ts new file mode 100644 index 0000000000000..13a59eb600db5 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/src/report_telemetry_counters.test.ts @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { firstValueFrom, Subject, take, toArray } from 'rxjs'; +import type { Event, TelemetryCounter } from '@kbn/analytics-client'; +import { TelemetryCounterType } from '@kbn/analytics-client'; +import { createTelemetryCounterHelper } from './report_telemetry_counters'; + +describe('reportTelemetryCounters', () => { + let reportTelemetryCounters: ReturnType; + let telemetryCounter$: Subject; + + beforeEach(() => { + telemetryCounter$ = new Subject(); + reportTelemetryCounters = createTelemetryCounterHelper(telemetryCounter$, 'my_shipper'); + }); + + test('emits a success counter for one event', async () => { + const events: Event[] = [ + { + timestamp: '2020-01-01T00:00:00.000Z', + event_type: 'event_type_a', + context: {}, + properties: {}, + }, + ]; + + const counters = firstValueFrom(telemetryCounter$); + + reportTelemetryCounters(events); + + await expect(counters).resolves.toMatchInlineSnapshot(` + Object { + "code": "OK", + "count": 1, + "event_type": "event_type_a", + "source": "my_shipper", + "type": "succeeded", + } + `); + }); + + test('emits a success counter for one event with custom code', async () => { + const events: Event[] = [ + { + timestamp: '2020-01-01T00:00:00.000Z', + event_type: 'event_type_a', + context: {}, + properties: {}, + }, + ]; + + const counters = firstValueFrom(telemetryCounter$); + + reportTelemetryCounters(events, { code: 'my_code' }); + + await expect(counters).resolves.toMatchInlineSnapshot(` + Object { + "code": "my_code", + "count": 1, + "event_type": "event_type_a", + "source": "my_shipper", + "type": "succeeded", + } + `); + }); + + test('emits a counter with custom type', async () => { + const events: Event[] = [ + { + timestamp: '2020-01-01T00:00:00.000Z', + event_type: 'event_type_a', + context: {}, + properties: {}, + }, + ]; + + const counters = firstValueFrom(telemetryCounter$); + + reportTelemetryCounters(events, { + type: TelemetryCounterType.dropped, + code: 'my_code', + }); + + await expect(counters).resolves.toMatchInlineSnapshot(` + Object { + "code": "my_code", + "count": 1, + "event_type": "event_type_a", + "source": "my_shipper", + "type": "dropped", + } + `); + }); + + test('emits a failure counter for one event with error message as a code', async () => { + const events: Event[] = [ + { + timestamp: '2020-01-01T00:00:00.000Z', + event_type: 'event_type_a', + context: {}, + properties: {}, + }, + ]; + + const counters = firstValueFrom(telemetryCounter$); + + reportTelemetryCounters(events, { + error: new Error('Something went terribly wrong'), + }); + + await expect(counters).resolves.toMatchInlineSnapshot(` + Object { + "code": "Something went terribly wrong", + "count": 1, + "event_type": "event_type_a", + "source": "my_shipper", + "type": "failed", + } + `); + }); + + test('emits a failure counter for one event with custom code', async () => { + const events: Event[] = [ + { + timestamp: '2020-01-01T00:00:00.000Z', + event_type: 'event_type_a', + context: {}, + properties: {}, + }, + ]; + + const counters = firstValueFrom(telemetryCounter$); + + reportTelemetryCounters(events, { + code: 'my_code', + error: new Error('Something went terribly wrong'), + }); + + await expect(counters).resolves.toMatchInlineSnapshot(` + Object { + "code": "my_code", + "count": 1, + "event_type": "event_type_a", + "source": "my_shipper", + "type": "failed", + } + `); + }); + + test('emits a success counter for multiple events of different types', async () => { + const events: Event[] = [ + // 2 types a + { + timestamp: '2020-01-01T00:00:00.000Z', + event_type: 'event_type_a', + context: {}, + properties: {}, + }, + { + timestamp: '2020-01-01T00:00:00.000Z', + event_type: 'event_type_a', + context: {}, + properties: {}, + }, + // 1 type b + { + timestamp: '2020-01-01T00:00:00.000Z', + event_type: 'event_type_b', + context: {}, + properties: {}, + }, + ]; + + const counters = firstValueFrom(telemetryCounter$.pipe(take(2), toArray())); + + reportTelemetryCounters(events); + + await expect(counters).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "code": "OK", + "count": 2, + "event_type": "event_type_a", + "source": "my_shipper", + "type": "succeeded", + }, + Object { + "code": "OK", + "count": 1, + "event_type": "event_type_b", + "source": "my_shipper", + "type": "succeeded", + }, + ] + `); + }); +}); diff --git a/packages/analytics/shippers/elastic_v3/common/src/report_telemetry_counters.ts b/packages/analytics/shippers/elastic_v3/common/src/report_telemetry_counters.ts new file mode 100644 index 0000000000000..e4b63bcf2710f --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/src/report_telemetry_counters.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 type { Subject } from 'rxjs'; +import type { Event, TelemetryCounter } from '@kbn/analytics-client'; +import { TelemetryCounterType } from '@kbn/analytics-client'; + +/** + * Creates a telemetry counter helper to make it easier to generate them + * @param telemetryCounter$ The observable that will be used to emit the telemetry counters + * @param source The name of the shipper that is sending the events. + */ +export function createTelemetryCounterHelper( + telemetryCounter$: Subject, + source: string +) { + /** + * Triggers a telemetry counter for each event type. + * @param events The events to trigger the telemetry counter for. + * @param type The type of telemetry counter to trigger. + * @param code The success or error code for additional detail about the result. + * @param error The error that occurred, if any. + */ + return ( + events: Event[], + { type, code, error }: { type?: TelemetryCounterType; code?: string; error?: Error } = {} + ) => { + const eventTypeCounts = countEventTypes(events); + Object.entries(eventTypeCounts).forEach(([eventType, count]) => { + telemetryCounter$.next({ + source, + type: type ?? (error ? TelemetryCounterType.failed : TelemetryCounterType.succeeded), + code: code ?? error?.message ?? 'OK', + count, + event_type: eventType, + }); + }); + }; +} + +function countEventTypes(events: Event[]) { + return events.reduce((acc, event) => { + if (acc[event.event_type]) { + acc[event.event_type] += 1; + } else { + acc[event.event_type] = 1; + } + return acc; + }, {} as Record); +} diff --git a/packages/analytics/shippers/elastic_v3/common/src/types.ts b/packages/analytics/shippers/elastic_v3/common/src/types.ts new file mode 100644 index 0000000000000..c27aa448119fe --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/src/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +/** + * Options for the Elastic V3 shipper + */ +export interface ElasticV3ShipperOptions { + /** + * The name of the channel to stream all the events to. + */ + channelName: string; + /** + * The product's version. + */ + version: string; + /** + * Provide it to override the Analytics client's default configuration. + */ + sendTo?: 'staging' | 'production'; + /** + * Should show debug information about the requests it makes to the V3 API. + */ + debug?: boolean; +} diff --git a/packages/analytics/shippers/elastic_v3/common/tsconfig.json b/packages/analytics/shippers/elastic_v3/common/tsconfig.json new file mode 100644 index 0000000000000..b81a9a0217634 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/common/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/analytics/shippers/elastic_v3/server/BUILD.bazel b/packages/analytics/shippers/elastic_v3/server/BUILD.bazel new file mode 100644 index 0000000000000..c05164d1f2b7e --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/server/BUILD.bazel @@ -0,0 +1,123 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "server" +PKG_REQUIRE_NAME = "@kbn/analytics-shippers-elastic-v3-server" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//node-fetch", + "@npm//rxjs", + "//packages/analytics/client", + "//packages/analytics/shippers/elastic_v3/common", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/node-fetch", + "@npm//@types/jest", + "@npm//rxjs", + "//packages/analytics/client:npm_module_types", + "//packages/analytics/shippers/elastic_v3/common:npm_module_types", + "//packages/kbn-logging-mocks:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/analytics/shippers/elastic_v3/server/README.md b/packages/analytics/shippers/elastic_v3/server/README.md new file mode 100644 index 0000000000000..ccdcf6a66cbe2 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/server/README.md @@ -0,0 +1,25 @@ +# @kbn/analytics-shippers-elastic-v3-server + +Server-side implementation of the Elastic V3 shipper for the `@kbn/analytics-client`. + +## How to use it + +This module is intended to be used **on the server-side only**. It is specially designed to apply the necessary backpressure mechanisms to prevent the server from getting overloaded with too many events and identify if the server sits behind a firewall to discard any incoming events. Refer to `@kbn/analytics-shippers-elastic-v3-browser` for the browser-side implementation. + +```typescript +import { ElasticV3ServerShipper } from "@kbn/analytics-shippers-elastic-v3-server"; + +analytics.registerShipper(ElasticV3ServerShipper, { channelName: 'myChannel', version: '1.0.0' }); +``` + +## Configuration + +| Name | Description | +|:-------------:|:-------------------------------------------------------------------------------------------| +| `channelName` | The name of the channel to send the events. | +| `version` | The version of the application generating the events. | +| `debug` | When `true`, it logs the responses from the remote Telemetry Service. Defaults to `false`. | + +## Transmission protocol + +This shipper sends the events to the Elastic Internal Telemetry Service. It holds up to 1000 events in a shared queue. Any additional incoming events once it's full will be dropped. It sends the events from the queue in batches of 10kB every 10 seconds. If not enough events are available in the queue for longer than 10 minutes, it will send any remaining events. When shutting down, it'll send all the remaining events in the queue. diff --git a/packages/analytics/shippers/elastic_v3/server/jest.config.js b/packages/analytics/shippers/elastic_v3/server/jest.config.js new file mode 100644 index 0000000000000..4397eb3e50c5a --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/server/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../../', + roots: ['/packages/analytics/shippers/elastic_v3/server'], +}; diff --git a/packages/analytics/shippers/elastic_v3/server/package.json b/packages/analytics/shippers/elastic_v3/server/package.json new file mode 100644 index 0000000000000..5ab24844eb1ba --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/server/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/analytics-shippers-elastic-v3-server", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/analytics/shippers/elastic_v3/server/src/index.ts b/packages/analytics/shippers/elastic_v3/server/src/index.ts new file mode 100644 index 0000000000000..50faf3c72ab29 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/server/src/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { ElasticV3ShipperOptions } from '@kbn/analytics-shippers-elastic-v3-common'; +export { ElasticV3ServerShipper } from './server_shipper'; diff --git a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.mocks.ts b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.mocks.ts new file mode 100644 index 0000000000000..8da36242d48b6 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.mocks.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. + */ + +export const fetchMock = jest.fn().mockResolvedValue({ + status: 200, + ok: true, + text: () => Promise.resolve('{"status": "ok"}'), +}); + +jest.doMock('node-fetch', () => fetchMock); diff --git a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts new file mode 100644 index 0000000000000..ffdfd797437a9 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts @@ -0,0 +1,438 @@ +/* + * Copyright 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 { loggerMock } from '@kbn/logging-mocks'; +import { firstValueFrom } from 'rxjs'; +import { fakeSchedulers } from 'rxjs-marbles/jest'; +import type { AnalyticsClientInitContext, Event } from '@kbn/analytics-client'; +import { fetchMock } from './server_shipper.test.mocks'; +import { ElasticV3ServerShipper } from './server_shipper'; + +const SECONDS = 1000; +const MINUTES = 60 * SECONDS; + +describe('ElasticV3ServerShipper', () => { + const events: Event[] = [ + { + timestamp: '2020-01-01T00:00:00.000Z', + event_type: 'test-event-type', + context: {}, + properties: {}, + }, + ]; + + const nextTick = () => new Promise((resolve) => setImmediate(resolve)); + + const initContext: AnalyticsClientInitContext = { + sendTo: 'staging', + isDev: true, + logger: loggerMock.create(), + }; + + let shipper: ElasticV3ServerShipper; + + // eslint-disable-next-line dot-notation + const setLastBatchSent = (ms: number) => (shipper['lastBatchSent'] = ms); + + beforeEach(() => { + jest.useFakeTimers(); + + shipper = new ElasticV3ServerShipper( + { version: '1.2.3', channelName: 'test-channel', debug: true }, + initContext + ); + // eslint-disable-next-line dot-notation + shipper['firstTimeOffline'] = null; + }); + + afterEach(() => { + shipper.shutdown(); + jest.clearAllMocks(); + }); + + test('set optIn should update the isOptedIn$ observable', () => { + // eslint-disable-next-line dot-notation + const getInternalOptIn = () => shipper['isOptedIn']; + + // Initially undefined + expect(getInternalOptIn()).toBeUndefined(); + + shipper.optIn(true); + expect(getInternalOptIn()).toBe(true); + + shipper.optIn(false); + expect(getInternalOptIn()).toBe(false); + }); + + test('clears the queue after optIn: false', () => { + shipper.reportEvents(events); + // eslint-disable-next-line dot-notation + expect(shipper['internalQueue'].length).toBe(1); + + shipper.optIn(false); + // eslint-disable-next-line dot-notation + expect(shipper['internalQueue'].length).toBe(0); + }); + + test('set extendContext should store local values: clusterUuid and licenseId', () => { + // eslint-disable-next-line dot-notation + const getInternalClusterUuid = () => shipper['clusterUuid']; + // eslint-disable-next-line dot-notation + const getInternalLicenseId = () => shipper['licenseId']; + + // Initial values + expect(getInternalClusterUuid()).toBe('UNKNOWN'); + expect(getInternalLicenseId()).toBeUndefined(); + + shipper.extendContext({ cluster_uuid: 'test-cluster-uuid' }); + expect(getInternalClusterUuid()).toBe('test-cluster-uuid'); + expect(getInternalLicenseId()).toBeUndefined(); + + shipper.extendContext({ license_id: 'test-license-id' }); + expect(getInternalClusterUuid()).toBe('test-cluster-uuid'); + expect(getInternalLicenseId()).toBe('test-license-id'); + + shipper.extendContext({ cluster_uuid: 'test-cluster-uuid-2', license_id: 'test-license-id-2' }); + expect(getInternalClusterUuid()).toBe('test-cluster-uuid-2'); + expect(getInternalLicenseId()).toBe('test-license-id-2'); + }); + + test('calls to reportEvents do not call `fetch` straight away', () => { + shipper.reportEvents(events); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + test( + 'calls to reportEvents do not call `fetch` after 10 minutes because no optIn value is set yet', + fakeSchedulers((advance) => { + shipper.reportEvents(events); + advance(10 * MINUTES); + expect(fetchMock).not.toHaveBeenCalled(); + }) + ); + + test( + 'calls to reportEvents call `fetch` after 10 minutes when optIn value is set to true', + fakeSchedulers(async (advance) => { + shipper.reportEvents(events); + shipper.optIn(true); + const counter = firstValueFrom(shipper.telemetryCounter$); + setLastBatchSent(Date.now() - 10 * MINUTES); + advance(10 * MINUTES); + expect(fetchMock).toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { + body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', + headers: { + 'content-type': 'application/x-njson', + 'x-elastic-cluster-id': 'UNKNOWN', + 'x-elastic-stack-version': '1.2.3', + }, + method: 'POST', + query: { debug: true }, + } + ); + await expect(counter).resolves.toMatchInlineSnapshot(` + Object { + "code": "200", + "count": 1, + "event_type": "test-event-type", + "source": "elastic_v3_server", + "type": "succeeded", + } + `); + }) + ); + + test( + 'calls to reportEvents do not call `fetch` after 10 minutes when optIn value is set to false', + fakeSchedulers((advance) => { + shipper.reportEvents(events); + shipper.optIn(false); + setLastBatchSent(Date.now() - 10 * MINUTES); + advance(10 * MINUTES); + expect(fetchMock).not.toHaveBeenCalled(); + }) + ); + + test('calls to reportEvents call `fetch` when shutting down if optIn value is set to true', async () => { + shipper.reportEvents(events); + shipper.optIn(true); + const counter = firstValueFrom(shipper.telemetryCounter$); + shipper.shutdown(); + await nextTick(); // We are handling the shutdown in a promise, so we need to wait for the next tick. + expect(fetchMock).toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { + body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', + headers: { + 'content-type': 'application/x-njson', + 'x-elastic-cluster-id': 'UNKNOWN', + 'x-elastic-stack-version': '1.2.3', + }, + method: 'POST', + query: { debug: true }, + } + ); + await expect(counter).resolves.toMatchInlineSnapshot(` + Object { + "code": "200", + "count": 1, + "event_type": "test-event-type", + "source": "elastic_v3_server", + "type": "succeeded", + } + `); + }); + + test( + 'does not add the query.debug: true property to the request if the shipper is not set with the debug flag', + fakeSchedulers((advance) => { + shipper = new ElasticV3ServerShipper( + { version: '1.2.3', channelName: 'test-channel' }, + initContext + ); + // eslint-disable-next-line dot-notation + shipper['firstTimeOffline'] = null; + shipper.reportEvents(events); + shipper.optIn(true); + setLastBatchSent(Date.now() - 10 * MINUTES); + advance(10 * MINUTES); + expect(fetchMock).toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { + body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', + headers: { + 'content-type': 'application/x-njson', + 'x-elastic-cluster-id': 'UNKNOWN', + 'x-elastic-stack-version': '1.2.3', + }, + method: 'POST', + } + ); + }) + ); + + test( + 'sends when the queue overflows the 10kB leaky bucket one batch every 10s', + fakeSchedulers(async (advance) => { + expect.assertions(2 * 9 + 2); + + shipper.reportEvents(new Array(1000).fill(events[0])); + shipper.optIn(true); + + // Due to the size of the test events, it matches 9 rounds. + for (let i = 0; i < 9; i++) { + const counter = firstValueFrom(shipper.telemetryCounter$); + setLastBatchSent(Date.now() - 10 * SECONDS); + advance(10 * SECONDS); + expect(fetchMock).toHaveBeenNthCalledWith( + i + 1, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { + body: new Array(103) + .fill( + '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n' + ) + .join(''), + headers: { + 'content-type': 'application/x-njson', + 'x-elastic-cluster-id': 'UNKNOWN', + 'x-elastic-stack-version': '1.2.3', + }, + method: 'POST', + query: { debug: true }, + } + ); + await expect(counter).resolves.toMatchInlineSnapshot(` + Object { + "code": "200", + "count": 103, + "event_type": "test-event-type", + "source": "elastic_v3_server", + "type": "succeeded", + } + `); + await nextTick(); + } + // eslint-disable-next-line dot-notation + expect(shipper['internalQueue'].length).toBe(1000 - 9 * 103); // 73 + + // If we call it again, it should not enqueue all the events (only the ones to fill the queue): + shipper.reportEvents(new Array(1000).fill(events[0])); + // eslint-disable-next-line dot-notation + expect(shipper['internalQueue'].length).toBe(1000); + }) + ); + + test( + 'handles when the fetch request fails', + fakeSchedulers(async (advance) => { + fetchMock.mockRejectedValueOnce(new Error('Failed to fetch')); + shipper.reportEvents(events); + shipper.optIn(true); + const counter = firstValueFrom(shipper.telemetryCounter$); + setLastBatchSent(Date.now() - 10 * MINUTES); + advance(10 * MINUTES); + expect(fetchMock).toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { + body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', + headers: { + 'content-type': 'application/x-njson', + 'x-elastic-cluster-id': 'UNKNOWN', + 'x-elastic-stack-version': '1.2.3', + }, + method: 'POST', + query: { debug: true }, + } + ); + await expect(counter).resolves.toMatchInlineSnapshot(` + Object { + "code": "Failed to fetch", + "count": 1, + "event_type": "test-event-type", + "source": "elastic_v3_server", + "type": "failed", + } + `); + }) + ); + + test( + 'handles when the fetch request fails (request completes but not OK response)', + fakeSchedulers(async (advance) => { + fetchMock.mockResolvedValueOnce({ + ok: false, + status: 400, + text: () => Promise.resolve('{"status": "not ok"}'), + }); + shipper.reportEvents(events); + shipper.optIn(true); + const counter = firstValueFrom(shipper.telemetryCounter$); + setLastBatchSent(Date.now() - 10 * MINUTES); + advance(10 * MINUTES); + expect(fetchMock).toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { + body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', + headers: { + 'content-type': 'application/x-njson', + 'x-elastic-cluster-id': 'UNKNOWN', + 'x-elastic-stack-version': '1.2.3', + }, + method: 'POST', + query: { debug: true }, + } + ); + await expect(counter).resolves.toMatchInlineSnapshot(` + Object { + "code": "400", + "count": 1, + "event_type": "test-event-type", + "source": "elastic_v3_server", + "type": "failed", + } + `); + }) + ); + + test( + 'connectivity check is run after report failure', + fakeSchedulers(async (advance) => { + fetchMock.mockRejectedValueOnce(new Error('Failed to fetch')); + shipper.reportEvents(events); + shipper.optIn(true); + const counter = firstValueFrom(shipper.telemetryCounter$); + setLastBatchSent(Date.now() - 10 * MINUTES); + advance(10 * MINUTES); + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { + body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', + headers: { + 'content-type': 'application/x-njson', + 'x-elastic-cluster-id': 'UNKNOWN', + 'x-elastic-stack-version': '1.2.3', + }, + method: 'POST', + query: { debug: true }, + } + ); + await expect(counter).resolves.toMatchInlineSnapshot(` + Object { + "code": "Failed to fetch", + "count": 1, + "event_type": "test-event-type", + "source": "elastic_v3_server", + "type": "failed", + } + `); + fetchMock.mockRejectedValueOnce(new Error('Failed to fetch')); + advance(1 * MINUTES); + await nextTick(); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + fetchMock.mockResolvedValueOnce({ ok: false }); + advance(2 * MINUTES); + await nextTick(); + expect(fetchMock).toHaveBeenNthCalledWith( + 3, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + + // let's see the effect of after 24 hours: + shipper.reportEvents(events); + // eslint-disable-next-line dot-notation + expect(shipper['internalQueue'].length).toBe(1); + // eslint-disable-next-line dot-notation + shipper['firstTimeOffline'] = 100; + + fetchMock.mockResolvedValueOnce({ ok: false }); + advance(4 * MINUTES); + await nextTick(); + expect(fetchMock).toHaveBeenNthCalledWith( + 4, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + // eslint-disable-next-line dot-notation + expect(shipper['internalQueue'].length).toBe(0); + + // New events are not added to the queue because it's been offline for 24 hours. + shipper.reportEvents(events); + // eslint-disable-next-line dot-notation + expect(shipper['internalQueue'].length).toBe(0); + + // Regains connection + fetchMock.mockResolvedValueOnce({ ok: true }); + advance(8 * MINUTES); + await nextTick(); + expect(fetchMock).toHaveBeenNthCalledWith( + 5, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + // eslint-disable-next-line dot-notation + expect(shipper['firstTimeOffline']).toBe(null); + + advance(16 * MINUTES); + await nextTick(); + expect(fetchMock).not.toHaveBeenNthCalledWith( + 6, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + }) + ); +}); diff --git a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts new file mode 100644 index 0000000000000..1c1277f7d7b4b --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts @@ -0,0 +1,290 @@ +/* + * Copyright 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 fetch from 'node-fetch'; +import { + filter, + Subject, + interval, + concatMap, + merge, + from, + firstValueFrom, + timer, + retryWhen, + tap, + delayWhen, + takeWhile, +} from 'rxjs'; +import type { + AnalyticsClientInitContext, + Event, + EventContext, + IShipper, + TelemetryCounter, +} from '@kbn/analytics-client'; +import { TelemetryCounterType } from '@kbn/analytics-client'; +import type { ElasticV3ShipperOptions } from '@kbn/analytics-shippers-elastic-v3-common'; +import { + buildHeaders, + buildUrl, + createTelemetryCounterHelper, + eventsToNDJSON, + ErrorWithCode, +} from '@kbn/analytics-shippers-elastic-v3-common'; + +const SECOND = 1000; +const MINUTE = 60 * SECOND; +const HOUR = 60 * MINUTE; +const KIB = 1024; +const MAX_NUMBER_OF_EVENTS_IN_INTERNAL_QUEUE = 1000; + +export class ElasticV3ServerShipper implements IShipper { + public static shipperName = 'elastic_v3_server'; + public readonly telemetryCounter$ = new Subject(); + + private readonly reportTelemetryCounters = createTelemetryCounterHelper( + this.telemetryCounter$, + ElasticV3ServerShipper.shipperName + ); + + private readonly internalQueue: Event[] = []; + private readonly shutdown$ = new Subject(); + + private readonly url: string; + + private lastBatchSent = Date.now(); + + private clusterUuid: string = 'UNKNOWN'; + private licenseId?: string; + private isOptedIn?: boolean; + + /** + * Specifies when it went offline: + * - `undefined` means it doesn't know yet whether it is online or offline + * - `null` means it's online + * - `number` means it's offline since that time + * @private + */ + private firstTimeOffline?: number | null; + + constructor( + private readonly options: ElasticV3ShipperOptions, + private readonly initContext: AnalyticsClientInitContext + ) { + this.url = buildUrl({ + sendTo: options.sendTo ?? initContext.sendTo, + channelName: options.channelName, + }); + this.setInternalSubscriber(); + this.checkConnectivity(); + } + + public extendContext(newContext: EventContext) { + if (newContext.cluster_uuid) { + this.clusterUuid = newContext.cluster_uuid; + } + if (newContext.license_id) { + this.licenseId = newContext.license_id; + } + } + + public optIn(isOptedIn: boolean) { + this.isOptedIn = isOptedIn; + + if (isOptedIn === false) { + this.internalQueue.length = 0; + } + } + + public reportEvents(events: Event[]) { + if ( + this.isOptedIn === false || + (this.firstTimeOffline && Date.now() - this.firstTimeOffline > 24 * HOUR) + ) { + return; + } + + const freeSpace = MAX_NUMBER_OF_EVENTS_IN_INTERNAL_QUEUE - this.internalQueue.length; + + // As per design, we only want store up-to 1000 events at a time. Drop anything that goes beyond that limit + if (freeSpace < events.length) { + const toDrop = events.length - freeSpace; + const droppedEvents = events.splice(-toDrop, toDrop); + this.reportTelemetryCounters(droppedEvents, { + type: TelemetryCounterType.dropped, + code: 'queue_full', + }); + } + + this.internalQueue.push(...events); + } + + public shutdown() { + this.shutdown$.complete(); + } + + /** + * Checks the server has connectivity to the remote endpoint. + * The frequency of the connectivity tests will back off, starting with 1 minute, and multiplying by 2 + * until it reaches 1 hour. Then, it’ll keep the 1h frequency until it reaches 24h without connectivity. + * At that point, it clears the queue and stops accepting events in the queue. + * The connectivity checks will continue to happen every 1 hour just in case it regains it at any point. + * @private + */ + private checkConnectivity() { + let backoff = 1 * MINUTE; + timer(0, 1 * MINUTE) + .pipe( + takeWhile(() => this.shutdown$.isStopped === false), + filter(() => this.isOptedIn === true && this.firstTimeOffline !== null), + concatMap(async () => { + const { ok } = await fetch(this.url, { + method: 'OPTIONS', + }); + + if (!ok) { + throw new Error(`Failed to connect to ${this.url}`); + } + + this.firstTimeOffline = null; + backoff = 1 * MINUTE; + }), + retryWhen((errors) => + errors.pipe( + takeWhile(() => this.shutdown$.isStopped === false), + tap(() => { + if (!this.firstTimeOffline) { + this.firstTimeOffline = Date.now(); + } else if (Date.now() - this.firstTimeOffline > 24 * HOUR) { + this.internalQueue.length = 0; + } + backoff = backoff * 2; + if (backoff > 1 * HOUR) { + backoff = 1 * HOUR; + } + }), + delayWhen(() => timer(backoff)) + ) + ) + ) + .subscribe(); + } + + private setInternalSubscriber() { + // Check the status of the queues every 1 second. + merge( + interval(1000), + // Using a promise because complete does not emit through the pipe. + from(firstValueFrom(this.shutdown$, { defaultValue: true })) + ) + .pipe( + // Only move ahead if it's opted-in and online. + filter(() => this.isOptedIn === true && this.firstTimeOffline === null), + + // Send the events now if (validations sorted from cheapest to most CPU expensive): + // - We are shutting down. + // - There are some events in the queue, and we didn't send anything in the last 10 minutes. + // - The last time we sent was more than 10 seconds ago and: + // - We reached the minimum batch size of 10kB per request in our leaky bucket. + // - The queue is full (meaning we'll never get to 10kB because the events are very small). + filter( + () => + (this.internalQueue.length > 0 && + (this.shutdown$.isStopped || Date.now() - this.lastBatchSent >= 10 * MINUTE)) || + (Date.now() - this.lastBatchSent >= 10 * SECOND && + (this.internalQueue.length === 1000 || + this.getQueueByteSize(this.internalQueue) >= 10 * KIB)) + ), + + // Send the events + concatMap(async () => { + this.lastBatchSent = Date.now(); + const eventsToSend = this.getEventsToSend(); + await this.sendEvents(eventsToSend); + }), + + // Stop the subscriber if we are shutting down. + takeWhile(() => !this.shutdown$.isStopped) + ) + .subscribe(); + } + + /** + * Calculates the size of the queue in bytes. + * @returns The number of bytes held in the queue. + * @private + */ + private getQueueByteSize(queue: Event[]) { + return queue.reduce((acc, event) => { + return acc + this.getEventSize(event); + }, 0); + } + + /** + * Calculates the size of the event in bytes. + * @param event The event to calculate the size of. + * @returns The number of bytes held in the event. + * @private + */ + private getEventSize(event: Event) { + return Buffer.from(JSON.stringify(event)).length; + } + + /** + * Returns a queue of events of up-to 10kB. + * @remarks It mutates the internal queue by removing from it the events returned by this method. + * @private + */ + private getEventsToSend(): Event[] { + // If the internal queue is already smaller than the minimum batch size, do a direct assignment. + if (this.getQueueByteSize(this.internalQueue) < 10 * KIB) { + return this.internalQueue.splice(0, this.internalQueue.length); + } + // Otherwise, we'll feed the events to the leaky bucket queue until we reach 10kB. + const queue: Event[] = []; + let queueByteSize = 0; + while (queueByteSize < 10 * KIB) { + const event = this.internalQueue.shift()!; + queueByteSize += this.getEventSize(event); + queue.push(event); + } + return queue; + } + + private async sendEvents(events: Event[]) { + try { + const code = await this.makeRequest(events); + this.reportTelemetryCounters(events, { code }); + } catch (error) { + this.reportTelemetryCounters(events, { code: error.code, error }); + this.firstTimeOffline = undefined; + } + } + + private async makeRequest(events: Event[]): Promise { + const { status, text, ok } = await fetch(this.url, { + method: 'POST', + body: eventsToNDJSON(events), + headers: buildHeaders(this.clusterUuid, this.options.version, this.licenseId), + ...(this.options.debug && { query: { debug: true } }), + }); + + if (this.options.debug) { + this.initContext.logger.debug( + `[${ElasticV3ServerShipper.shipperName}]: ${status} - ${await text()}` + ); + } + + if (!ok) { + throw new ErrorWithCode(`${status} - ${await text()}`, `${status}`); + } + + return `${status}`; + } +} diff --git a/packages/analytics/shippers/elastic_v3/server/tsconfig.json b/packages/analytics/shippers/elastic_v3/server/tsconfig.json new file mode 100644 index 0000000000000..b81a9a0217634 --- /dev/null +++ b/packages/analytics/shippers/elastic_v3/server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/analytics/shippers/fullstory/BUILD.bazel b/packages/analytics/shippers/fullstory/BUILD.bazel index 2825e3fd733ea..c7da842e3cd93 100644 --- a/packages/analytics/shippers/fullstory/BUILD.bazel +++ b/packages/analytics/shippers/fullstory/BUILD.bazel @@ -62,6 +62,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -86,7 +93,7 @@ ts_project( js_library( name = PKG_DIRNAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/analytics/shippers/fullstory/README.md b/packages/analytics/shippers/fullstory/README.md index 60c4b641e300b..6d1c443db3d22 100644 --- a/packages/analytics/shippers/fullstory/README.md +++ b/packages/analytics/shippers/fullstory/README.md @@ -4,7 +4,7 @@ FullStory implementation as a shipper for the `@kbn/analytics-client`. ## How to use it -This module is intended to be used on the UI only. It does not support server-side events. +This module is intended to be used **on the browser only**. It does not support server-side events. ```typescript import { FullStoryShipper } from "@kbn/analytics-shippers-fullstory"; @@ -21,3 +21,11 @@ analytics.registerShipper(FullStoryShipper, { fullStoryOrgId: '12345' }) | `scriptUrl` | The URL to load the FullStory client from. Falls back to `edge.fullstory.com/s/fs.js` if not specified. | | `debug` | Whether the debug logs should be printed to the console. Defaults to `false`. | | `namespace` | The name of the variable where the API is stored: `window[namespace]`. Defaults to `FS`. | + +## FullStory Custom Events Rate Limits + +FullStory limits the number of custom events that can be sent per second ([docs](https://help.fullstory.com/hc/en-us/articles/360020623234#custom-property-rate-limiting)). In order to comply with that limit, this shipper will only emit the event types registered in the allow-list defined in the constant [CUSTOM_EVENT_TYPES_ALLOWLIST](./src/fullstory_shipper.ts). We may change this behaviour in the future to a remotely-controlled list of events or rely on the opt-in _cherry-pick_ config mechanism of the Analytics Client. + +## Transmission protocol + +This shipper relies on FullStory official snippet. The internals about how it transfers the data are not documented. diff --git a/packages/analytics/shippers/fullstory/package.json b/packages/analytics/shippers/fullstory/package.json index 5d8fa7b7df976..ab5ac56b35641 100644 --- a/packages/analytics/shippers/fullstory/package.json +++ b/packages/analytics/shippers/fullstory/package.json @@ -2,6 +2,7 @@ "name": "@kbn/analytics-shippers-fullstory", "private": true, "version": "1.0.0", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "license": "SSPL-1.0 OR Elastic License 2.0" } diff --git a/packages/analytics/shippers/fullstory/src/fullstory_shipper.test.ts b/packages/analytics/shippers/fullstory/src/fullstory_shipper.test.ts index 4b60ba661fe14..10f24ba5b5e14 100644 --- a/packages/analytics/shippers/fullstory/src/fullstory_shipper.test.ts +++ b/packages/analytics/shippers/fullstory/src/fullstory_shipper.test.ts @@ -119,7 +119,7 @@ describe('FullStoryShipper', () => { { event_type: 'test-event-2', timestamp: '2020-01-01T00:00:00.000Z', - properties: { test: 'test-2' }, + properties: { other_property: 'test-2' }, context: { pageName: 'test-page-1' }, }, ]); @@ -129,6 +129,49 @@ describe('FullStoryShipper', () => { test_str: 'test-1', }); expect(fullStoryApiMock.event).toHaveBeenCalledWith('test-event-2', { + other_property_str: 'test-2', + }); + }); + + test('filters the events by the allow-list', () => { + fullstoryShipper = new FullStoryShipper( + { + eventTypesAllowlist: ['valid-event-1', 'valid-event-2'], + debug: true, + fullStoryOrgId: 'test-org-id', + }, + { + logger: loggerMock.create(), + sendTo: 'staging', + isDev: true, + } + ); + fullstoryShipper.reportEvents([ + { + event_type: 'test-event-1', // Should be filtered out. + timestamp: '2020-01-01T00:00:00.000Z', + properties: { test: 'test-1' }, + context: { pageName: 'test-page-1' }, + }, + { + event_type: 'valid-event-1', + timestamp: '2020-01-01T00:00:00.000Z', + properties: { test: 'test-1' }, + context: { pageName: 'test-page-1' }, + }, + { + event_type: 'valid-event-2', + timestamp: '2020-01-01T00:00:00.000Z', + properties: { test: 'test-2' }, + context: { pageName: 'test-page-1' }, + }, + ]); + + expect(fullStoryApiMock.event).toHaveBeenCalledTimes(2); + expect(fullStoryApiMock.event).toHaveBeenCalledWith('valid-event-1', { + test_str: 'test-1', + }); + expect(fullStoryApiMock.event).toHaveBeenCalledWith('valid-event-2', { test_str: 'test-2', }); }); diff --git a/packages/analytics/shippers/fullstory/src/fullstory_shipper.ts b/packages/analytics/shippers/fullstory/src/fullstory_shipper.ts index ef9d5d662813d..48e8d7e6fa246 100644 --- a/packages/analytics/shippers/fullstory/src/fullstory_shipper.ts +++ b/packages/analytics/shippers/fullstory/src/fullstory_shipper.ts @@ -18,20 +18,46 @@ import { getParsedVersion } from './get_parsed_version'; import { formatPayload } from './format_payload'; import { loadSnippet } from './load_snippet'; -export type FullStoryShipperConfig = FullStorySnippetConfig; +/** + * FullStory shipper configuration. + */ +export interface FullStoryShipperConfig extends FullStorySnippetConfig { + /** + * FullStory's custom events rate limit is very aggressive. + * If this setting is provided, it'll only send the event types specified in this list. + */ + eventTypesAllowlist?: string[]; +} +/** + * FullStory shipper. + */ export class FullStoryShipper implements IShipper { + /** Shipper's unique name */ public static shipperName = 'FullStory'; + private readonly fullStoryApi: FullStoryApi; private lastUserId: string | undefined; + private readonly eventTypesAllowlist?: string[]; + /** + * Creates a new instance of the FullStoryShipper. + * @param config {@link FullStoryShipperConfig} + * @param initContext {@link AnalyticsClientInitContext} + */ constructor( config: FullStoryShipperConfig, private readonly initContext: AnalyticsClientInitContext ) { - this.fullStoryApi = loadSnippet(config); + const { eventTypesAllowlist, ...snippetConfig } = config; + this.fullStoryApi = loadSnippet(snippetConfig); + this.eventTypesAllowlist = eventTypesAllowlist; } + /** + * {@inheritDoc IShipper.extendContext} + * @param newContext {@inheritDoc IShipper.extendContext.newContext} + */ public extendContext(newContext: EventContext): void { this.initContext.logger.debug(`Received context ${JSON.stringify(newContext)}`); @@ -70,6 +96,10 @@ export class FullStoryShipper implements IShipper { } } + /** + * {@inheritDoc IShipper.optIn} + * @param isOptedIn {@inheritDoc IShipper.optIn.isOptedIn} + */ public optIn(isOptedIn: boolean): void { this.initContext.logger.debug(`Setting FS to optIn ${isOptedIn}`); // FullStory uses 2 different opt-in methods: @@ -85,11 +115,21 @@ export class FullStoryShipper implements IShipper { } } + /** + * {@inheritDoc IShipper.reportEvents} + * @param events {@inheritDoc IShipper.reportEvents.events} + */ public reportEvents(events: Event[]): void { this.initContext.logger.debug(`Reporting ${events.length} events to FS`); - events.forEach((event) => { - // We only read event.properties and discard the rest because the context is already sent in the other APIs. - this.fullStoryApi.event(event.event_type, formatPayload(event.properties)); - }); + events + .filter((event) => this.eventTypesAllowlist?.includes(event.event_type) ?? true) + .forEach((event) => { + // We only read event.properties and discard the rest because the context is already sent in the other APIs. + this.fullStoryApi.event(event.event_type, formatPayload(event.properties)); + }); + } + + public shutdown() { + // No need to do anything here for now. } } diff --git a/packages/analytics/shippers/fullstory/src/load_snippet.ts b/packages/analytics/shippers/fullstory/src/load_snippet.ts index 471152f033b5a..b06530111d672 100644 --- a/packages/analytics/shippers/fullstory/src/load_snippet.ts +++ b/packages/analytics/shippers/fullstory/src/load_snippet.ts @@ -8,6 +8,9 @@ import type { FullStoryApi } from './types'; +/** + * FullStory basic configuration. + */ export interface FullStorySnippetConfig { /** * The FullStory account id. diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/aggregators/service_latency_aggregator.ts b/packages/elastic-apm-synthtrace/src/lib/apm/aggregators/service_latency_aggregator.ts new file mode 100644 index 0000000000000..e28ba234b2a49 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/apm/aggregators/service_latency_aggregator.ts @@ -0,0 +1,183 @@ +/* + * Copyright 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 { random } from 'lodash'; +import { Client } from '@elastic/elasticsearch'; +import { ApmFields } from '../apm_fields'; +import { Fields } from '../../entity'; +import { StreamAggregator } from '../../stream_aggregator'; + +type LatencyState = { + count: number; + min: number; + max: number; + sum: number; + timestamp: number; +} & Pick; + +export type ServiceFields = Fields & + Pick< + ApmFields, + | 'timestamp.us' + | 'ecs.version' + | 'metricset.name' + | 'observer' + | 'processor.event' + | 'processor.name' + | 'service.name' + | 'service.version' + | 'service.environment' + | 'transaction.type' + > & + Partial<{ + 'transaction.duration.aggregate': { + min: number; + max: number; + sum: number; + value_count: number; + }; + }>; + +export class ServiceLatencyAggregator implements StreamAggregator { + public readonly name; + + constructor() { + this.name = 'service-latency'; + } + + getDataStreamName(): string { + return 'metrics-apm.service'; + } + + getMappings(): Record { + return { + properties: { + '@timestamp': { + type: 'date', + format: 'date_optional_time||epoch_millis', + }, + transaction: { + type: 'object', + properties: { + type: { type: 'keyword', time_series_dimension: true }, + duration: { + type: 'object', + properties: { + aggregate: { + type: 'aggregate_metric_double', + metrics: ['min', 'max', 'sum', 'value_count'], + default_metric: 'sum', + time_series_metric: 'gauge', + }, + }, + }, + }, + }, + service: { + type: 'object', + properties: { + name: { type: 'keyword', time_series_dimension: true }, + environment: { type: 'keyword', time_series_dimension: true }, + }, + }, + }, + }; + } + + getDimensions(): string[] { + return ['service.name', 'service.environment', 'transaction.type']; + } + + getWriteTarget(document: Record): string | null { + const eventType = document.metricset?.name; + if (eventType === 'service') return 'metrics-apm.service-default'; + return null; + } + + private state: Record = {}; + + private processedComponent: number = 0; + + process(event: ApmFields): Fields[] | null { + if (!event['@timestamp']) return null; + const service = event['service.name']!; + const environment = event['service.environment'] ?? 'production'; + const transactionType = event['transaction.type'] ?? 'request'; + const key = `${service}-${environment}-${transactionType}`; + const addToState = (timestamp: number) => { + if (!this.state[key]) { + this.state[key] = { + timestamp, + count: 0, + min: 0, + max: 0, + sum: 0, + 'service.name': service, + 'service.environment': environment, + 'transaction.type': transactionType, + }; + } + const duration = Number(event['transaction.duration.us']); + if (duration >= 0) { + const state = this.state[key]; + + state.count++; + state.sum += duration; + if (duration > state.max) state.max = duration; + if (duration < state.min) state.min = Math.min(0, duration); + } + }; + + // ensure we flush current state first if event falls out of the current max window age + if (this.state[key]) { + const diff = Math.abs(event['@timestamp'] - this.state[key].timestamp); + if (diff >= 1000 * 60) { + const fields = this.createServiceFields(key); + delete this.state[key]; + addToState(event['@timestamp']); + return [fields]; + } + } + + addToState(event['@timestamp']); + // if cardinality is too high force emit of current state + if (Object.keys(this.state).length === 1000) { + return this.flush(); + } + + return null; + } + + flush(): Fields[] { + const fields = Object.keys(this.state).map((key) => this.createServiceFields(key)); + this.state = {}; + return fields; + } + + private createServiceFields(key: string): ServiceFields { + this.processedComponent = ++this.processedComponent % 1000; + const component = Date.now() % 100; + const state = this.state[key]; + return { + '@timestamp': state.timestamp + random(0, 100) + component + this.processedComponent, + 'metricset.name': 'service', + 'processor.event': 'metric', + 'service.name': state['service.name'], + 'service.environment': state['service.environment'], + 'transaction.type': state['transaction.type'], + 'transaction.duration.aggregate': { + min: state.min, + max: state.max, + sum: state.sum, + value_count: state.count, + }, + }; + } + + async bootstrapElasticsearch(esClient: Client): Promise {} +} diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client.ts b/packages/elastic-apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client.ts index 6e7fb5ffdb1bc..91bec0ba49c52 100644 --- a/packages/elastic-apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/client/apm_synthtrace_es_client.ts @@ -7,6 +7,7 @@ */ import { Client } from '@elastic/elasticsearch'; +import { IndicesIndexSettings } from '@elastic/elasticsearch/lib/api/types'; import { cleanWriteTargets } from '../../utils/clean_write_targets'; import { getApmWriteTargets } from '../utils/get_apm_write_targets'; import { Logger } from '../../utils/create_logger'; @@ -15,6 +16,7 @@ import { EntityIterable } from '../../entity_iterable'; import { StreamProcessor } from '../../stream_processor'; import { EntityStreams } from '../../entity_streams'; import { Fields } from '../../entity'; +import { StreamAggregator } from '../../stream_aggregator'; export interface StreamToBulkOptions { concurrency?: number; @@ -57,7 +59,7 @@ export class ApmSynthtraceEsClient { return info.version.number; } - async clean() { + async clean(dataStreams?: string[]) { return this.getWriteTargets().then(async (writeTargets) => { const indices = Object.values(writeTargets); this.logger.info(`Attempting to clean: ${indices}`); @@ -68,7 +70,7 @@ export class ApmSynthtraceEsClient { logger: this.logger, }); } - for (const name of indices) { + for (const name of indices.concat(dataStreams ?? [])) { const dataStream = await this.client.indices.getDataStream({ name }, { ignore: [404] }); if (dataStream.data_streams && dataStream.data_streams.length > 0) { this.logger.debug(`Deleting datastream: ${name}`); @@ -149,7 +151,6 @@ export class ApmSynthtraceEsClient { streamProcessor?: StreamProcessor ) { const dataStream = Array.isArray(events) ? new EntityStreams(events) : events; - const sp = streamProcessor != null ? streamProcessor @@ -165,7 +166,7 @@ export class ApmSynthtraceEsClient { await this.logger.perf('enumerate_scenario', async () => { // @ts-ignore // We just want to enumerate - for await (item of sp.streamToDocumentAsync(sp.toDocument, dataStream)) { + for await (item of sp.streamToDocumentAsync((e) => sp.toDocument(e), dataStream)) { if (yielded === 0) { options.itemStartStopCallback?.apply(this, [item, false]); yielded++; @@ -185,7 +186,7 @@ export class ApmSynthtraceEsClient { flushBytes: 500000, // TODO https://github.com/elastic/elasticsearch-js/issues/1610 // having to map here is awkward, it'd be better to map just before serialization. - datasource: sp.streamToDocumentAsync(sp.toDocument, dataStream), + datasource: sp.streamToDocumentAsync((e) => sp.toDocument(e), dataStream), onDrop: (doc) => { this.logger.info(JSON.stringify(doc, null, 2)); }, @@ -197,11 +198,12 @@ export class ApmSynthtraceEsClient { options?.itemStartStopCallback?.apply(this, [item, false]); yielded++; } - const index = options?.mapToIndex - ? options?.mapToIndex(item) - : !this.forceLegacyIndices - ? StreamProcessor.getDataStreamForEvent(item, writeTargets) - : StreamProcessor.getIndexForEvent(item, writeTargets); + let index = options?.mapToIndex ? options?.mapToIndex(item) : null; + if (!index) { + index = !this.forceLegacyIndices + ? sp.getDataStreamForEvent(item, writeTargets) + : StreamProcessor.getIndexForEvent(item, writeTargets); + } return { create: { _index: index } }; }, }); @@ -211,4 +213,53 @@ export class ApmSynthtraceEsClient { await this.refresh(); } } + + async createDataStream(aggregator: StreamAggregator) { + const datastreamName = aggregator.getDataStreamName(); + const mappings = aggregator.getMappings(); + const dimensions = aggregator.getDimensions(); + + const indexSettings: IndicesIndexSettings = { lifecycle: { name: 'metrics' } }; + if (dimensions.length > 0) { + indexSettings.mode = 'time_series'; + indexSettings.routing_path = dimensions; + } + + await this.client.cluster.putComponentTemplate({ + name: `${datastreamName}-mappings`, + template: { + mappings, + }, + _meta: { + description: `Mappings for ${datastreamName}-*`, + }, + }); + this.logger.info(`Created mapping component template for ${datastreamName}-*`); + + await this.client.cluster.putComponentTemplate({ + name: `${datastreamName}-settings`, + template: { + settings: { + index: indexSettings, + }, + }, + _meta: { + description: `Settings for ${datastreamName}-*`, + }, + }); + this.logger.info(`Created settings component template for ${datastreamName}-*`); + + await this.client.indices.putIndexTemplate({ + name: `${datastreamName}-index_template`, + index_patterns: [`${datastreamName}-*`], + data_stream: {}, + composed_of: [`${datastreamName}-mappings`, `${datastreamName}-settings`], + priority: 500, + }); + this.logger.info(`Created index template for ${datastreamName}-*`); + + await this.client.indices.createDataStream({ name: datastreamName + '-default' }); + + await aggregator.bootstrapElasticsearch(this.client); + } } diff --git a/packages/elastic-apm-synthtrace/src/lib/stream_aggregator.ts b/packages/elastic-apm-synthtrace/src/lib/stream_aggregator.ts new file mode 100644 index 0000000000000..3076b105a10fd --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/lib/stream_aggregator.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 { Client } from '@elastic/elasticsearch'; +import { ApmFields, Fields } from '..'; + +export interface StreamAggregator { + name: string; + + getWriteTarget(document: Record): string | null; + + process(event: TFields): Fields[] | null; + + flush(): Fields[]; + + bootstrapElasticsearch(esClient: Client): Promise; + + getDataStreamName(): string; + + getDimensions(): string[]; + + getMappings(): Record; +} diff --git a/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts b/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts index 17ced20f5d7ed..e1cb332996e23 100644 --- a/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts +++ b/packages/elastic-apm-synthtrace/src/lib/stream_processor.ts @@ -17,10 +17,12 @@ import { dedot } from './utils/dedot'; import { ApmElasticsearchOutputWriteTargets } from './apm/utils/get_apm_write_targets'; import { Logger } from './utils/create_logger'; import { Fields } from './entity'; +import { StreamAggregator } from './stream_aggregator'; export interface StreamProcessorOptions { version?: string; - processors: Array<(events: TFields[]) => TFields[]>; + processors?: Array<(events: TFields[]) => TFields[]>; + streamAggregators?: Array>; flushInterval?: string; // defaults to 10k maxBufferSize?: number; @@ -39,6 +41,8 @@ export class StreamProcessor { getBreakdownMetrics, ]; public static defaultFlushInterval: number = 10000; + private readonly processors: Array<(events: TFields[]) => TFields[]>; + private readonly streamAggregators: Array>; constructor(private readonly options: StreamProcessorOptions) { [this.intervalAmount, this.intervalUnit] = this.options.flushInterval @@ -47,6 +51,8 @@ export class StreamProcessor { this.name = this.options?.name ?? 'StreamProcessor'; this.version = this.options.version ?? '8.0.0'; this.versionMajor = Number.parseInt(this.version.split('.')[0], 10); + this.processors = options.processors ?? []; + this.streamAggregators = options.streamAggregators ?? []; } private readonly intervalAmount: number; private readonly intervalUnit: any; @@ -73,6 +79,15 @@ export class StreamProcessor { yield StreamProcessor.enrich(event, this.version, this.versionMajor); sourceEventsYielded++; + for (const aggregator of this.streamAggregators) { + const aggregatedEvents = aggregator.process(event); + if (aggregatedEvents) { + yield* aggregatedEvents.map((d) => + StreamProcessor.enrich(d, this.version, this.versionMajor) + ); + } + } + if (sourceEventsYielded % maxBufferSize === 0) { if (this.options?.processedCallback) { this.options.processedCallback(maxBufferSize); @@ -96,7 +111,7 @@ export class StreamProcessor { this.options.logger?.debug( `${this.name} flush ${localBuffer.length} documents ${order}: ${e} => ${f}` ); - for (const processor of this.options.processors) { + for (const processor of this.processors) { yield* processor(localBuffer).map((d) => StreamProcessor.enrich(d, this.version, this.versionMajor) ); @@ -116,13 +131,16 @@ export class StreamProcessor { this.options.logger?.info( `${this.name} processing remaining buffer: ${localBuffer.length} items left` ); - for (const processor of this.options.processors) { + for (const processor of this.processors) { yield* processor(localBuffer).map((d) => StreamProcessor.enrich(d, this.version, this.versionMajor) ); } this.options.processedCallback?.apply(this, [localBuffer.length]); } + for (const aggregator of this.streamAggregators) { + yield* aggregator.flush(); + } } private calculateFlushAfter(eventDate: number | null, order: 'asc' | 'desc') { @@ -186,10 +204,7 @@ export class StreamProcessor { return newDoc; } - static getDataStreamForEvent( - d: Record, - writeTargets: ApmElasticsearchOutputWriteTargets - ) { + getDataStreamForEvent(d: Record, writeTargets: ApmElasticsearchOutputWriteTargets) { if (!d.processor?.event) { throw Error("'processor.event' is not set on document, can not determine target index"); } @@ -204,6 +219,13 @@ export class StreamProcessor { } } } + for (const aggregator of this.streamAggregators) { + const target = aggregator.getWriteTarget(d); + if (target) { + dataStream = target; + break; + } + } return dataStream; } diff --git a/packages/elastic-apm-synthtrace/src/scripts/run_synthtrace.ts b/packages/elastic-apm-synthtrace/src/scripts/run_synthtrace.ts index 7ba3251def983..5e007d9adeac4 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/run_synthtrace.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/run_synthtrace.ts @@ -14,6 +14,8 @@ import { startLiveDataUpload } from './utils/start_live_data_upload'; import { parseRunCliFlags } from './utils/parse_run_cli_flags'; import { getCommonServices } from './utils/get_common_services'; import { ApmSynthtraceKibanaClient } from '../lib/apm/client/apm_synthtrace_kibana_client'; +import { StreamAggregator } from '../lib/stream_aggregator'; +import { ServiceLatencyAggregator } from '../lib/apm/aggregators/service_latency_aggregator'; function options(y: Argv) { return y @@ -186,8 +188,9 @@ yargs(process.argv.slice(2)) await apmEsClient.updateComponentTemplates(runOptions.numShards); } + const aggregators: StreamAggregator[] = [new ServiceLatencyAggregator()]; if (argv.clean) { - await apmEsClient.clean(); + await apmEsClient.clean(aggregators.map((a) => a.getDataStreamName() + '-*')); } if (runOptions.gcpRepository) { await apmEsClient.registerGcpRepository(runOptions.gcpRepository); @@ -205,6 +208,8 @@ yargs(process.argv.slice(2)) )}` ); + for (const aggregator of aggregators) await apmEsClient.createDataStream(aggregator); + if (runOptions.maxDocs !== 0) await startHistoricalDataUpload(apmEsClient, logger, runOptions, from, to, version); diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/worker.ts b/packages/elastic-apm-synthtrace/src/scripts/utils/synthtrace_worker.ts similarity index 92% rename from packages/elastic-apm-synthtrace/src/scripts/utils/worker.ts rename to packages/elastic-apm-synthtrace/src/scripts/utils/synthtrace_worker.ts index 4e4ef8a02ff71..76b6e2ce6b6d8 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/worker.ts +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/synthtrace_worker.ts @@ -15,6 +15,8 @@ import { LogLevel } from '../../lib/utils/create_logger'; import { StreamProcessor } from '../../lib/stream_processor'; import { Scenario } from '../scenario'; import { EntityIterable, Fields } from '../..'; +import { StreamAggregator } from '../../lib/stream_aggregator'; +import { ServiceLatencyAggregator } from '../../lib/apm/aggregators/service_latency_aggregator'; // logging proxy to main thread, ensures we see real time logging const l = { @@ -61,9 +63,11 @@ async function setup() { parentPort?.postMessage({ workerIndex, lastTimestamp: item['@timestamp'] }); } }; + const aggregators: StreamAggregator[] = [new ServiceLatencyAggregator()]; streamProcessor = new StreamProcessor({ version, processors: StreamProcessor.apmProcessors, + streamAggregators: aggregators, maxSourceEvents: runOptions.maxDocs, logger: l, processedCallback: (processedDocuments) => { diff --git a/packages/elastic-apm-synthtrace/src/scripts/utils/worker.js b/packages/elastic-apm-synthtrace/src/scripts/utils/worker.js index 84887f4d9285b..734a9323e5516 100644 --- a/packages/elastic-apm-synthtrace/src/scripts/utils/worker.js +++ b/packages/elastic-apm-synthtrace/src/scripts/utils/worker.js @@ -12,4 +12,4 @@ require('@babel/register')({ presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], }); -require('./worker'); +require('./synthtrace_worker'); diff --git a/packages/kbn-ace/BUILD.bazel b/packages/kbn-ace/BUILD.bazel index 630a636e99b9b..8db4f8c4a4a21 100644 --- a/packages/kbn-ace/BUILD.bazel +++ b/packages/kbn-ace/BUILD.bazel @@ -52,6 +52,19 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + additional_args = [ + "--copy-files", + "--ignore", + "**/*/src/ace/modes/x_json/worker/x_json.ace.worker.js", + "--quiet" + ], + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -77,7 +90,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-ace/package.json b/packages/kbn-ace/package.json index f5775c3b34596..ce8e83e0686ef 100644 --- a/packages/kbn-ace/package.json +++ b/packages/kbn-ace/package.json @@ -2,6 +2,7 @@ "name": "@kbn/ace", "version": "1.0.0", "private": true, + "browser": "./target_web/index.js", "main": "./target_node/index.js", "license": "SSPL-1.0 OR Elastic License 2.0" } diff --git a/packages/kbn-babel-preset/styled_components_files.js b/packages/kbn-babel-preset/styled_components_files.js index a8b1234a406fd..53052809b6b2f 100644 --- a/packages/kbn-babel-preset/styled_components_files.js +++ b/packages/kbn-babel-preset/styled_components_files.js @@ -13,7 +13,7 @@ module.exports = { */ USES_STYLED_COMPONENTS: [ /packages[\/\\]kbn-ui-shared-deps-(npm|src)[\/\\]/, - /src[\/\\]plugins[\/\\](unified_search|kibana_react)[\/\\]/, + /src[\/\\]plugins[\/\\](kibana_react)[\/\\]/, /x-pack[\/\\]plugins[\/\\](apm|beats_management|cases|fleet|infra|lists|observability|osquery|security_solution|timelines|synthetics|ux)[\/\\]/, /x-pack[\/\\]test[\/\\]plugin_functional[\/\\]plugins[\/\\]resolver_test[\/\\]/, ], diff --git a/packages/kbn-bazel-packages/src/bazel_package_dirs.ts b/packages/kbn-bazel-packages/src/bazel_package_dirs.ts index f09883994feca..80248646f1e6f 100644 --- a/packages/kbn-bazel-packages/src/bazel_package_dirs.ts +++ b/packages/kbn-bazel-packages/src/bazel_package_dirs.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ +import globby from 'globby'; import Path from 'path'; -import globby from 'globby'; import { REPO_ROOT } from '@kbn/utils'; /** @@ -24,6 +24,7 @@ export const BAZEL_PACKAGE_DIRS = [ 'packages/shared-ux/*', 'packages/analytics', 'packages/analytics/shippers', + 'packages/analytics/shippers/elastic_v3', ]; /** diff --git a/packages/kbn-bazel-packages/src/discover_packages.ts b/packages/kbn-bazel-packages/src/discover_packages.ts index db31b54beec75..8b78e4e293118 100644 --- a/packages/kbn-bazel-packages/src/discover_packages.ts +++ b/packages/kbn-bazel-packages/src/discover_packages.ts @@ -9,6 +9,7 @@ import Path from 'path'; import globby from 'globby'; +import normalizePath from 'normalize-path'; import { REPO_ROOT } from '@kbn/utils'; import { asyncMapWithLimit } from '@kbn/std'; @@ -16,7 +17,7 @@ import { BazelPackage } from './bazel_package'; import { BAZEL_PACKAGE_DIRS } from './bazel_package_dirs'; export function discoverBazelPackageLocations(repoRoot: string) { - return globby + const packagesWithPackageJson = globby .sync( BAZEL_PACKAGE_DIRS.map((dir) => `${dir}/*/package.json`), { @@ -24,8 +25,26 @@ export function discoverBazelPackageLocations(repoRoot: string) { absolute: true, } ) + // NOTE: removing x-pack by default for now to prevent a situation where a BUILD.bazel file + // needs to be added at the root of the folder which will make x-pack to be wrongly recognized + // as a Bazel package in that case + .filter((path) => !normalizePath(path).includes('x-pack/package.json')) .sort((a, b) => a.localeCompare(b)) .map((path) => Path.dirname(path)); + + const packagesWithBuildBazel = globby + .sync( + BAZEL_PACKAGE_DIRS.map((dir) => `${dir}/*/BUILD.bazel`), + { + cwd: repoRoot, + absolute: true, + } + ) + .map((path) => Path.dirname(path)); + + // NOTE: only return as discovered packages the ones with a package.json + BUILD.bazel file. + // In the future we should change this to only discover the ones declaring kibana.json. + return packagesWithPackageJson.filter((pkg) => packagesWithBuildBazel.includes(pkg)); } export async function discoverBazelPackages(repoRoot: string = REPO_ROOT) { diff --git a/packages/kbn-dev-utils/src/proc_runner/proc.ts b/packages/kbn-dev-utils/src/proc_runner/proc.ts index 0402feec99d47..c622c46456abf 100644 --- a/packages/kbn-dev-utils/src/proc_runner/proc.ts +++ b/packages/kbn-dev-utils/src/proc_runner/proc.ts @@ -37,19 +37,7 @@ async function withTimeout( ms: number, onTimeout: () => Promise ) { - const TIMEOUT = Symbol('timeout'); - try { - await Promise.race([ - attempt(), - new Promise((_, reject) => setTimeout(() => reject(TIMEOUT), ms)), - ]); - } catch (error) { - if (error === TIMEOUT) { - await onTimeout(); - } else { - throw error; - } - } + await Rx.lastValueFrom(Rx.race(Rx.defer(attempt), Rx.timer(ms).pipe(Rx.mergeMap(onTimeout)))); } export type Proc = ReturnType; diff --git a/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts b/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts index 3b7cf008df9a6..857a4fcfd475d 100644 --- a/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts +++ b/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts @@ -7,7 +7,6 @@ */ import * as Rx from 'rxjs'; -import { filter, first, catchError, map } from 'rxjs/operators'; import exitHook from 'exit-hook'; import { ToolingLog } from '@kbn/tooling-log'; @@ -93,29 +92,30 @@ export class ProcRunner { try { if (wait instanceof RegExp) { // wait for process to log matching line - await Rx.race( - proc.lines$.pipe( - filter((line) => wait.test(line)), - first(), - catchError((err) => { - if (err.name !== 'EmptyError') { - throw createFailError(`[${name}] exited without matching pattern: ${wait}`); - } else { - throw err; - } - }) - ), - waitTimeout === false - ? Rx.NEVER - : Rx.timer(waitTimeout).pipe( - map(() => { - const sec = waitTimeout / SECOND; - throw createFailError( - `[${name}] failed to match pattern within ${sec} seconds [pattern=${wait}]` - ); - }) - ) - ).toPromise(); + await Rx.lastValueFrom( + Rx.race( + proc.lines$.pipe( + Rx.filter((line) => wait.test(line)), + Rx.take(1), + Rx.defaultIfEmpty(undefined), + Rx.map((line) => { + if (line === undefined) { + throw createFailError(`[${name}] exited without matching pattern: ${wait}`); + } + }) + ), + waitTimeout === false + ? Rx.NEVER + : Rx.timer(waitTimeout).pipe( + Rx.map(() => { + const sec = waitTimeout / SECOND; + throw createFailError( + `[${name}] failed to match pattern within ${sec} seconds [pattern=${wait}]` + ); + }) + ) + ) + ); } if (wait === true) { diff --git a/packages/kbn-doc-links/BUILD.bazel b/packages/kbn-doc-links/BUILD.bazel index 13b68935c4326..25f376afba96b 100644 --- a/packages/kbn-doc-links/BUILD.bazel +++ b/packages/kbn-doc-links/BUILD.bazel @@ -45,6 +45,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -69,7 +76,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-doc-links/package.json b/packages/kbn-doc-links/package.json index 8ddbe564c356c..e4212ed989d9a 100644 --- a/packages/kbn-doc-links/package.json +++ b/packages/kbn-doc-links/package.json @@ -1,5 +1,6 @@ { "name": "@kbn/doc-links", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 14fd80c3a8552..79b41112768a6 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -47,6 +47,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { metaData: `${APM_DOCS}guide/${DOC_LINK_VERSION}/data-model-metadata.html`, overview: `${APM_DOCS}guide/${DOC_LINK_VERSION}/apm-overview.html`, tailSamplingPolicies: `${APM_DOCS}guide/${DOC_LINK_VERSION}/configure-tail-based-sampling.html`, + elasticAgent: `${APM_DOCS}guide/${DOC_LINK_VERSION}/upgrade-to-apm-integration.html`, }, canvas: { guide: `${KIBANA_DOCS}canvas.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 4a5a9fdeb9576..645aad3af2bd2 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -33,6 +33,7 @@ export interface DocLinks { readonly metaData: string; readonly overview: string; readonly tailSamplingPolicies: string; + readonly elasticAgent: string; }; readonly canvas: { readonly guide: string; diff --git a/packages/kbn-es-archiver/src/cli.ts b/packages/kbn-es-archiver/src/cli.ts index fbb5784afe5ac..899a7843a68fc 100644 --- a/packages/kbn-es-archiver/src/cli.ts +++ b/packages/kbn-es-archiver/src/cli.ts @@ -24,7 +24,7 @@ import { Client, HttpConnection } from '@elastic/elasticsearch'; import { EsArchiver } from './es_archiver'; const resolveConfigPath = (v: string) => Path.resolve(process.cwd(), v); -const defaultConfigPath = resolveConfigPath('test/functional/config.js'); +const defaultConfigPath = resolveConfigPath('test/functional/config.base.js'); export function runCli() { new RunWithCommands({ diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index a6faffc2cfcd7..50ca9fa91e0aa 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -260,6 +260,24 @@ exports.Cluster = class Cluster { await this._outcome; } + /** + * Stops ES process, it it's running, without waiting for it to shutdown gracefully + */ + async kill() { + if (this._stopCalled) { + return; + } + + this._stopCalled; + + if (!this._process || !this._outcome) { + throw new Error('ES has not been started'); + } + + await treeKillAsync(this._process.pid, 'SIGKILL'); + await this._outcome; + } + /** * Common logic from this.start() and this.run() * diff --git a/packages/kbn-field-types/BUILD.bazel b/packages/kbn-field-types/BUILD.bazel index 77a4acaedb235..4259b9c14a6ab 100644 --- a/packages/kbn-field-types/BUILD.bazel +++ b/packages/kbn-field-types/BUILD.bazel @@ -43,10 +43,17 @@ TYPES_DEPS = [ ] jsts_transpiler( - name = "target_node", - srcs = SRCS, - build_pkg_name = package_name(), - ) + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) ts_config( name = "tsconfig", @@ -72,7 +79,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-field-types/package.json b/packages/kbn-field-types/package.json index 4e6276e508c36..14b842526d9bc 100644 --- a/packages/kbn-field-types/package.json +++ b/packages/kbn-field-types/package.json @@ -3,5 +3,6 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", + "browser": "./target_web/index.js", "main": "./target_node/index.js" } diff --git a/packages/kbn-import-resolver/src/import_resolver.ts b/packages/kbn-import-resolver/src/import_resolver.ts index 44114ce731e28..05b69f299a798 100644 --- a/packages/kbn-import-resolver/src/import_resolver.ts +++ b/packages/kbn-import-resolver/src/import_resolver.ts @@ -25,9 +25,18 @@ const NODE_MODULE_SEG = Path.sep + 'node_modules' + Path.sep; export class ImportResolver { static create(repoRoot: string) { const pkgMap = new Map(); - for (const dir of discoverBazelPackageLocations(repoRoot)) { - const pkg = JSON.parse(Fs.readFileSync(Path.resolve(dir, 'package.json'), 'utf8')); - pkgMap.set(pkg.name, normalizePath(Path.relative(repoRoot, dir))); + for (const dir of discoverBazelPackageLocations(REPO_ROOT)) { + const relativeBazelPackageDir = Path.relative(REPO_ROOT, dir); + const repoRootBazelPackageDir = Path.resolve(repoRoot, relativeBazelPackageDir); + + if (!Fs.existsSync(Path.resolve(repoRootBazelPackageDir, 'package.json'))) { + continue; + } + + const pkg = JSON.parse( + Fs.readFileSync(Path.resolve(repoRootBazelPackageDir, 'package.json'), 'utf8') + ); + pkgMap.set(pkg.name, normalizePath(relativeBazelPackageDir)); } return new ImportResolver(repoRoot, pkgMap, readPackageMap()); diff --git a/packages/kbn-interpreter/BUILD.bazel b/packages/kbn-interpreter/BUILD.bazel index 469f16296230f..f1c066d4cbd84 100644 --- a/packages/kbn-interpreter/BUILD.bazel +++ b/packages/kbn-interpreter/BUILD.bazel @@ -42,6 +42,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + peggy( name = "grammar", data = [ @@ -82,7 +89,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES + [":grammar"], - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index 1480e7e808370..ca1f35c02874b 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -1,6 +1,7 @@ { "name": "@kbn/interpreter", "author": "App Services", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", diff --git a/packages/kbn-io-ts-utils/BUILD.bazel b/packages/kbn-io-ts-utils/BUILD.bazel index aa0116b81efe6..15917e75a5285 100644 --- a/packages/kbn-io-ts-utils/BUILD.bazel +++ b/packages/kbn-io-ts-utils/BUILD.bazel @@ -49,6 +49,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -73,7 +80,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-io-ts-utils/package.json b/packages/kbn-io-ts-utils/package.json index 2dc3532e05d96..806f3c46cf337 100644 --- a/packages/kbn-io-ts-utils/package.json +++ b/packages/kbn-io-ts-utils/package.json @@ -1,5 +1,6 @@ { "name": "@kbn/io-ts-utils", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", diff --git a/packages/kbn-mapbox-gl/BUILD.bazel b/packages/kbn-mapbox-gl/BUILD.bazel index 89cbeb4c431ae..d4cebd52152f0 100644 --- a/packages/kbn-mapbox-gl/BUILD.bazel +++ b/packages/kbn-mapbox-gl/BUILD.bazel @@ -45,6 +45,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -69,7 +76,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-mapbox-gl/package.json b/packages/kbn-mapbox-gl/package.json index 493fbb881c80c..f0a5c7eabdfcb 100644 --- a/packages/kbn-mapbox-gl/package.json +++ b/packages/kbn-mapbox-gl/package.json @@ -3,5 +3,6 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", + "browser": "./target_web/index.js", "main": "./target_node/index.js" } diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 9f73dcd620d30..9baed7a92a53e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -57,7 +57,7 @@ pageLoadAssetSize: telemetry: 51957 telemetryManagementSection: 38586 transform: 41007 - triggersActionsUi: 103400 + triggersActionsUi: 104400 upgradeAssistant: 81241 urlForwarding: 32579 usageCollection: 39762 diff --git a/packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts b/packages/kbn-optimizer/src/audit_bundle_dependencies/find_babel_runtime_helpers_in_entry_bundles.ts similarity index 100% rename from packages/kbn-optimizer/src/babel_runtime_helpers/find_babel_runtime_helpers_in_entry_bundles.ts rename to packages/kbn-optimizer/src/audit_bundle_dependencies/find_babel_runtime_helpers_in_entry_bundles.ts diff --git a/packages/kbn-optimizer/src/babel_runtime_helpers/find_node_libs_browser_polyfills_in_entry_bundles.ts b/packages/kbn-optimizer/src/audit_bundle_dependencies/find_node_libs_browser_polyfills_in_entry_bundles.ts similarity index 100% rename from packages/kbn-optimizer/src/babel_runtime_helpers/find_node_libs_browser_polyfills_in_entry_bundles.ts rename to packages/kbn-optimizer/src/audit_bundle_dependencies/find_node_libs_browser_polyfills_in_entry_bundles.ts diff --git a/packages/kbn-optimizer/src/audit_bundle_dependencies/find_target_node_imports.ts b/packages/kbn-optimizer/src/audit_bundle_dependencies/find_target_node_imports.ts new file mode 100644 index 0000000000000..dd1309f838e41 --- /dev/null +++ b/packages/kbn-optimizer/src/audit_bundle_dependencies/find_target_node_imports.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; + +import { run } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; + +import { OptimizerConfig } from '../optimizer'; +import { parseStats } from './parse_stats'; + +/** + * Analyzes the bundle dependencies to find any imports using the `@kbn//target_node` build target. + * + * We should aim for those packages to be imported using the `@kbn//target_web` build because it's optimized + * for browser compatibility. + * + * This utility also helps identify when code that should only run in the server is leaked into the browser. + */ +export async function runFindTargetNodeImportsCli() { + run(async ({ log }) => { + const config = OptimizerConfig.create({ + includeCoreBundle: true, + repoRoot: REPO_ROOT, + }); + + const paths = config.bundles.map((b) => Path.resolve(b.outputDir, 'stats.json')); + + log.info('analyzing', paths.length, 'stats files'); + log.verbose(paths); + + const imports = new Set(); + for (const path of paths) { + const stats = parseStats(path); + + for (const module of stats.modules) { + if (module.name.includes('/target_node/')) { + const [, cleanName] = /\/((?:kbn-|@kbn\/).+)\/target_node/.exec(module.name) ?? []; + imports.add(cleanName || module.name); + } + } + } + + log.success('found', imports.size, '@kbn/*/target_node imports in entry bundles and chunks'); + log.write( + Array.from(imports, (i) => `'${i}',`) + .sort() + .join('\n') + ); + }); +} diff --git a/packages/kbn-optimizer/src/babel_runtime_helpers/index.ts b/packages/kbn-optimizer/src/audit_bundle_dependencies/index.ts similarity index 91% rename from packages/kbn-optimizer/src/babel_runtime_helpers/index.ts rename to packages/kbn-optimizer/src/audit_bundle_dependencies/index.ts index 3a7987f867bc5..e6059c4c2c9b5 100644 --- a/packages/kbn-optimizer/src/babel_runtime_helpers/index.ts +++ b/packages/kbn-optimizer/src/audit_bundle_dependencies/index.ts @@ -8,3 +8,4 @@ export * from './find_babel_runtime_helpers_in_entry_bundles'; export * from './find_node_libs_browser_polyfills_in_entry_bundles'; +export * from './find_target_node_imports'; diff --git a/packages/kbn-optimizer/src/babel_runtime_helpers/parse_stats.ts b/packages/kbn-optimizer/src/audit_bundle_dependencies/parse_stats.ts similarity index 100% rename from packages/kbn-optimizer/src/babel_runtime_helpers/parse_stats.ts rename to packages/kbn-optimizer/src/audit_bundle_dependencies/parse_stats.ts diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts index d759a4aa02455..48e77c8628905 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -14,4 +14,4 @@ export * from './node'; export * from './limits'; export * from './cli'; export * from './report_optimizer_timings'; -export * from './babel_runtime_helpers'; +export * from './audit_bundle_dependencies'; diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_built_paths.ts b/packages/kbn-optimizer/src/optimizer/optimizer_built_paths.ts index 294c3e835a3bd..8421c0846d52a 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_built_paths.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_built_paths.ts @@ -14,7 +14,10 @@ import { ascending } from '../common'; export async function getOptimizerBuiltPaths() { return ( await globby( - ['**/*', '!**/{__fixtures__,__snapshots__,integration_tests,babel_runtime_helpers,node}/**'], + [ + '**/*', + '!**/{__fixtures__,__snapshots__,integration_tests,audit_bundle_dependencies,node}/**', + ], { cwd: Path.resolve(__dirname, '../'), absolute: true, diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 422fe12f11b28..876bc347a21c5 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -94,7 +94,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: module: { // no parse rules for a few known large packages which have no require() statements // or which have require() statements that should be ignored because the file is - // already bundled with all its necessary depedencies + // already bundled with all its necessary dependencies noParse: [ /[\/\\]node_modules[\/\\]lodash[\/\\]index\.js$/, /[\/\\]node_modules[\/\\]vega[\/\\]build[\/\\]vega\.js$/, diff --git a/packages/kbn-performance-testing-dataset-extractor/BUILD.bazel b/packages/kbn-performance-testing-dataset-extractor/BUILD.bazel new file mode 100644 index 0000000000000..b58375165352c --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/BUILD.bazel @@ -0,0 +1,122 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "kbn-performance-testing-dataset-extractor" +PKG_REQUIRE_NAME = "@kbn/performance-testing-dataset-extractor" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "//packages/kbn-dev-utils", + "//packages/kbn-utils", + "//packages/kbn-tooling-log", + "@npm//@elastic/elasticsearch", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "//packages/kbn-dev-utils:npm_module_types", + "//packages/kbn-utils:npm_module_types", + "//packages/kbn-tooling-log:npm_module_types", + "@npm//@elastic/elasticsearch", + "@npm//@types/node", + "@npm//@types/jest", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-performance-testing-dataset-extractor/README.md b/packages/kbn-performance-testing-dataset-extractor/README.md new file mode 100644 index 0000000000000..ef5488a82ff21 --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/README.md @@ -0,0 +1,14 @@ +# @kbn/performance-testing-dataset-extractor + +A library to convert APM traces into JSON format for performance testing. + +## Usage + +``` + node scripts/extract_performance_testing_dataset \ + --journeyName "<_source.labels.journeyName>" \ + --buildId "<_source.labels.testBuildId>" \ + --es-url "" \ + --es-username "" \ + --es-password "" +``` \ No newline at end of file diff --git a/packages/kbn-performance-testing-dataset-extractor/jest.config.js b/packages/kbn-performance-testing-dataset-extractor/jest.config.js new file mode 100644 index 0000000000000..e31a2d7996893 --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-performance-testing-dataset-extractor'], +}; diff --git a/packages/kbn-performance-testing-dataset-extractor/package.json b/packages/kbn-performance-testing-dataset-extractor/package.json new file mode 100644 index 0000000000000..4d637728b28de --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/package.json @@ -0,0 +1,11 @@ +{ + "name": "@kbn/performance-testing-dataset-extractor", + "description": "A library to convert APM traces into JSON format for performance testing.", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0", + "kibana": { + "devOnly": true + } +} diff --git a/packages/kbn-performance-testing-dataset-extractor/src/cli.ts b/packages/kbn-performance-testing-dataset-extractor/src/cli.ts new file mode 100644 index 0000000000000..7d16f625e4874 --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/src/cli.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 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. + */ + +/** *********************************************************** + * + * Run `node scripts/extract_performance_testing_dataset --help` for usage information + * + *************************************************************/ + +import { run, createFlagError } from '@kbn/dev-utils'; +import { extractor } from './extractor'; + +export async function runExtractor() { + run( + async ({ log, flags }) => { + const baseURL = flags['es-url']; + if (baseURL && typeof baseURL !== 'string') { + throw createFlagError('--es-url must be a string'); + } + if (!baseURL) { + throw createFlagError('--es-url must be defined'); + } + + const username = flags['es-username']; + if (username && typeof username !== 'string') { + throw createFlagError('--es-username must be a string'); + } + if (!username) { + throw createFlagError('--es-username must be defined'); + } + + const password = flags['es-password']; + if (password && typeof password !== 'string') { + throw createFlagError('--es-password must be a string'); + } + if (!password) { + throw createFlagError('--es-password must be defined'); + } + + const journeyName = flags.journeyName; + if (journeyName && typeof journeyName !== 'string') { + throw createFlagError('--journeyName must be a string'); + } + if (!journeyName) { + throw createFlagError('--journeyName must be defined'); + } + + const buildId = flags.buildId; + if (buildId && typeof buildId !== 'string') { + throw createFlagError('--buildId must be a string'); + } + if (!buildId) { + throw createFlagError('--buildId must be defined'); + } + + return extractor({ + param: { journeyName, buildId }, + client: { baseURL, username, password }, + log, + }); + }, + { + description: `CLI to fetch and normalize APM traces for journey scalability testing`, + flags: { + string: ['journeyName', 'buildId', 'es-url', 'es-username', 'es-password'], + help: ` + --journeyName Single user performance journey name, stored in APM-based document as label: 'labels.journeyName' + --buildId BUILDKITE_JOB_ID or uuid generated locally, stored in APM-based document as label: 'labels.testBuildId' + --es-url url for Elasticsearch (APM cluster) + --es-username username for Elasticsearch (APM cluster) + --es-password password for Elasticsearch (APM cluster) + `, + }, + } + ); +} diff --git a/packages/kbn-performance-testing-dataset-extractor/src/es_client.ts b/packages/kbn-performance-testing-dataset-extractor/src/es_client.ts new file mode 100644 index 0000000000000..53c2e8ba9e8c3 --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/src/es_client.ts @@ -0,0 +1,148 @@ +/* + * Copyright 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 { Client } from '@elastic/elasticsearch'; + +interface ClientOptions { + node: string; + username: string; + password: string; +} + +interface Labels { + journeyName: string; + maxUsersCount: string; +} + +interface Request { + method: string; + headers: string; + body?: { original: string }; +} + +interface Response { + status_code: number; +} + +interface Transaction { + id: string; + name: string; + type: string; +} + +export interface Document { + labels: Labels; + character: string; + quote: string; + service: { version: string }; + processor: string; + trace: { id: string }; + '@timestamp': string; + environment: string; + url: { path: string }; + http: { + request: Request; + response: Response; + }; + transaction: Transaction; +} + +export function initClient(options: ClientOptions) { + const client = new Client({ + node: options.node, + auth: { + username: options.username, + password: options.password, + }, + }); + + return { + async getTransactions(buildId: string, journeyName: string) { + const result = await client.search({ + body: { + track_total_hits: true, + sort: [ + { + '@timestamp': { + order: 'desc', + unmapped_type: 'boolean', + }, + }, + ], + size: 10000, + stored_fields: ['*'], + _source: true, + query: { + bool: { + must: [], + filter: [ + { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + 'transaction.type': 'request', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match_phrase: { + 'processor.event': 'transaction', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match_phrase: { + 'labels.testBuildId': buildId, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match_phrase: { + 'labels.journeyName': journeyName, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + should: [], + must_not: [], + }, + }, + }, + }); + return result?.hits?.hits; + }, + }; +} diff --git a/packages/kbn-performance-testing-dataset-extractor/src/extractor.ts b/packages/kbn-performance-testing-dataset-extractor/src/extractor.ts new file mode 100644 index 0000000000000..cd0dbed9d8e51 --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/src/extractor.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import fs from 'fs/promises'; +import { existsSync } from 'fs'; +import path from 'path'; +import { ToolingLog } from '@kbn/tooling-log'; +import { initClient, Document } from './es_client'; + +interface CLIParams { + param: { + journeyName: string; + buildId: string; + }; + client: { + baseURL: string; + username: string; + password: string; + }; + log: ToolingLog; +} + +export const extractor = async ({ param, client, log }: CLIParams) => { + const authOptions = { + node: client.baseURL, + username: client.username, + password: client.password, + }; + const esClient = initClient(authOptions); + const hits = await esClient.getTransactions(param.buildId, param.journeyName); + if (!hits || hits.length === 0) { + log.warning(` + No transactions found with 'labels.testBuildId=${param.buildId}' and 'labels.journeyName=${param.journeyName}' + \nOutput file won't be generated + `); + return; + } + + const source = hits[0]!._source as Document; + const journeyName = source.labels.journeyName || 'Unknown Journey'; + const kibanaVersion = source.service.version; + const maxUsersCount = source.labels.maxUsersCount || '0'; + + const data = hits + .map((hit) => hit!._source as Document) + .map((hit) => { + return { + processor: hit.processor, + traceId: hit.trace.id, + timestamp: hit['@timestamp'], + environment: hit.environment, + request: { + url: { path: hit.url.path }, + headers: hit.http.request.headers, + method: hit.http.request.method, + body: hit.http.request.body ? JSON.parse(hit.http.request.body.original) : '', + }, + response: { statusCode: hit.http.response.status_code }, + transaction: { + id: hit.transaction.id, + name: hit.transaction.name, + type: hit.transaction.type, + }, + }; + }); + + const output = { + journeyName, + kibanaVersion, + maxUsersCount, + traceItems: data, + }; + + const outputDir = path.resolve('target/scalability_traces'); + const fileName = `${output.journeyName.replace(/ /g, '')}-${param.buildId}.json`; + const filePath = path.resolve(outputDir, fileName); + + log.info(`Found ${hits.length} transactions, output file: ${filePath}`); + if (!existsSync(outputDir)) { + await fs.mkdir(outputDir, { recursive: true }); + } + await fs.writeFile(filePath, JSON.stringify(output, null, 2), 'utf8'); +}; diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/index.ts b/packages/kbn-performance-testing-dataset-extractor/src/index.ts similarity index 84% rename from src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/index.ts rename to packages/kbn-performance-testing-dataset-extractor/src/index.ts index c4ce88e1a851a..4e739789d65af 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/index.ts +++ b/packages/kbn-performance-testing-dataset-extractor/src/index.ts @@ -6,4 +6,5 @@ * Side Public License, v 1. */ -export { registerUiCountersRollups } from './register_rollups'; +export { extractor } from './extractor'; +export * from './cli'; diff --git a/packages/kbn-performance-testing-dataset-extractor/tsconfig.json b/packages/kbn-performance-testing-dataset-extractor/tsconfig.json new file mode 100644 index 0000000000000..a8cfc2cceb08b --- /dev/null +++ b/packages/kbn-performance-testing-dataset-extractor/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 6638acfef5ef4..184c16f96167f 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -2270,8 +2270,6 @@ exports.observeLines = observeLines; var Rx = _interopRequireWildcard(__webpack_require__("../../node_modules/rxjs/dist/esm5/index.js")); -var _operators = __webpack_require__("../../node_modules/rxjs/dist/esm5/operators/index.js"); - var _observe_readable = __webpack_require__("../../node_modules/@kbn/stdio-dev-helpers/target_node/observe_readable.js"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } @@ -2297,8 +2295,8 @@ const SEP = /\r?\n/; * @return {Rx.Observable} */ function observeLines(readable) { - const done$ = (0, _observe_readable.observeReadable)(readable).pipe((0, _operators.share)()); - const scan$ = Rx.fromEvent(readable, 'data').pipe((0, _operators.scan)(({ + const done$ = (0, _observe_readable.observeReadable)(readable).pipe(Rx.share()); + const scan$ = Rx.fromEvent(readable, 'data').pipe(Rx.scan(({ buffer }, chunk) => { buffer += chunk; @@ -2322,16 +2320,15 @@ function observeLines(readable) { }, { buffer: '' }), // stop if done completes or errors - (0, _operators.takeUntil)(done$.pipe((0, _operators.materialize)())), (0, _operators.share)()); + Rx.takeUntil(done$.pipe(Rx.materialize())), Rx.share()); return Rx.merge( // use done$ to provide completion/errors done$, // merge in the "lines" from each step - scan$.pipe((0, _operators.mergeMap)(({ + scan$.pipe(Rx.mergeMap(({ lines }) => lines || [])), // inject the "unsplit" data at the end - scan$.pipe((0, _operators.last)(), (0, _operators.mergeMap)(({ + scan$.pipe(Rx.takeLast(1), Rx.mergeMap(({ buffer - }) => buffer ? [buffer] : []), // if there were no lines, last() will error, so catch and complete - (0, _operators.catchError)(() => Rx.empty()))); + }) => buffer ? [buffer] : []))); } /***/ }), @@ -2349,8 +2346,6 @@ exports.observeReadable = observeReadable; var Rx = _interopRequireWildcard(__webpack_require__("../../node_modules/rxjs/dist/esm5/index.js")); -var _operators = __webpack_require__("../../node_modules/rxjs/dist/esm5/operators/index.js"); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } @@ -2369,7 +2364,9 @@ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && * - fails on the first "error" event */ function observeReadable(readable) { - return Rx.race(Rx.fromEvent(readable, 'end').pipe((0, _operators.first)(), (0, _operators.ignoreElements)()), Rx.fromEvent(readable, 'error').pipe((0, _operators.first)(), (0, _operators.mergeMap)(err => Rx.throwError(err)))); + return Rx.race(Rx.fromEvent(readable, 'end').pipe(Rx.first(), Rx.ignoreElements()), Rx.fromEvent(readable, 'error').pipe(Rx.first(), Rx.map(err => { + throw err; + }))); } /***/ }), @@ -61566,32 +61563,6 @@ class Project { return this.json.name; } - ensureValidProjectDependency(project) { - const relativePathToProject = normalizePath(path__WEBPACK_IMPORTED_MODULE_1___default.a.relative(this.path, project.path)); - const relativePathToProjectIfBazelPkg = normalizePath(path__WEBPACK_IMPORTED_MODULE_1___default.a.relative(this.path, `${__dirname}/../../../bazel-bin/packages/${path__WEBPACK_IMPORTED_MODULE_1___default.a.basename(project.path)}`)); - const versionInPackageJson = this.allDependencies[project.name]; - const expectedVersionInPackageJson = `link:${relativePathToProject}`; - const expectedVersionInPackageJsonIfBazelPkg = `link:${relativePathToProjectIfBazelPkg}`; // TODO: after introduce bazel to build all the packages and completely remove the support for kbn packages - // do not allow child projects to hold dependencies, unless they are meant to be published externally - - if (versionInPackageJson === expectedVersionInPackageJson || versionInPackageJson === expectedVersionInPackageJsonIfBazelPkg) { - return; - } - - const updateMsg = 'Update its package.json to the expected value below.'; - const meta = { - actual: `"${project.name}": "${versionInPackageJson}"`, - expected: `"${project.name}": "${expectedVersionInPackageJson}" or "${project.name}": "${expectedVersionInPackageJsonIfBazelPkg}"`, - package: `${this.name} (${this.packageJsonLocation})` - }; - - if (Object(_package_json__WEBPACK_IMPORTED_MODULE_5__[/* isLinkDependency */ "a"])(versionInPackageJson)) { - throw new _errors__WEBPACK_IMPORTED_MODULE_3__[/* CliError */ "a"](`[${this.name}] depends on [${project.name}] using 'link:', but the path is wrong. ${updateMsg}`, meta); - } - - throw new _errors__WEBPACK_IMPORTED_MODULE_3__[/* CliError */ "a"](`[${this.name}] depends on [${project.name}] but it's not using the local package. ${updateMsg}`, meta); - } - getBuildConfig() { return this.json.kibana && this.json.kibana.build || {}; } @@ -61663,10 +61634,6 @@ class Project { return Object.values(this.allDependencies).every(dep => Object(_package_json__WEBPACK_IMPORTED_MODULE_5__[/* isLinkDependency */ "a"])(dep)); } -} // We normalize all path separators to `/` in generated files - -function normalizePath(path) { - return path.replace(/[\\\/]+/g, '/'); } /***/ }), @@ -61688,7 +61655,8 @@ function normalizePath(path) { /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__("util"); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__("./src/utils/errors.ts"); -/* harmony import */ var _project__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__("./src/utils/project.ts"); +/* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__("./src/utils/log.ts"); +/* harmony import */ var _project__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__("./src/utils/project.ts"); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License @@ -61701,6 +61669,7 @@ function normalizePath(path) { + const glob = Object(util__WEBPACK_IMPORTED_MODULE_2__["promisify"])(glob__WEBPACK_IMPORTED_MODULE_0___default.a); /** a Map of project names to Project instances */ @@ -61719,7 +61688,7 @@ async function getProjects(rootPath, projectsPathsPatterns, { for (const filePath of pathsToProcess) { const projectConfigPath = normalize(filePath); const projectDir = path__WEBPACK_IMPORTED_MODULE_1___default.a.dirname(projectConfigPath); - const project = await _project__WEBPACK_IMPORTED_MODULE_4__[/* Project */ "a"].fromPath(projectDir); + const project = await _project__WEBPACK_IMPORTED_MODULE_5__[/* Project */ "a"].fromPath(projectDir); const excludeProject = exclude.includes(project.name) || include.length > 0 && !include.includes(project.name) || bazelOnly && !project.isBazelPackage(); if (excludeProject) { @@ -61793,10 +61762,18 @@ function buildProjectGraph(projects) { const projectDeps = []; const dependencies = project.allDependencies; + if (!project.isSinglePackageJsonProject && Object.keys(dependencies).length > 0) { + _log__WEBPACK_IMPORTED_MODULE_4__[/* log */ "a"].warning(`${project.name} is not allowed to hold local dependencies and they will be discarded. Please declare them at the root package.json`); + } + + if (!project.isSinglePackageJsonProject) { + projectGraph.set(project.name, projectDeps); + continue; + } + for (const depName of Object.keys(dependencies)) { if (projects.has(depName)) { const dep = projects.get(depName); - project.ensureValidProjectDependency(dep); projectDeps.push(dep); } } diff --git a/packages/kbn-pm/src/__snapshots__/run.test.ts.snap b/packages/kbn-pm/src/__snapshots__/run.test.ts.snap index e5efc9a915224..28e1b98e0fcd9 100644 --- a/packages/kbn-pm/src/__snapshots__/run.test.ts.snap +++ b/packages/kbn-pm/src/__snapshots__/run.test.ts.snap @@ -4,14 +4,9 @@ exports[`excludes project if single \`exclude\` filter is specified 1`] = ` Object { "graph": Object { "bar": Array [], - "baz": Array [ - "bar", - ], + "baz": Array [], "kibana": Array [], - "quux": Array [ - "bar", - "baz", - ], + "quux": Array [], "with-additional-projects": Array [], }, "projects": Array [ @@ -42,12 +37,8 @@ Object { exports[`includes only projects specified in multiple \`include\` filters 1`] = ` Object { "graph": Object { - "bar": Array [ - "foo", - ], - "baz": Array [ - "bar", - ], + "bar": Array [], + "baz": Array [], "foo": Array [], }, "projects": Array [ @@ -72,20 +63,13 @@ Object { exports[`passes all found projects to the command if no filter is specified 1`] = ` Object { "graph": Object { - "bar": Array [ - "foo", - ], - "baz": Array [ - "bar", - ], + "bar": Array [], + "baz": Array [], "foo": Array [], "kibana": Array [ "foo", ], - "quux": Array [ - "bar", - "baz", - ], + "quux": Array [], "with-additional-projects": Array [], }, "projects": Array [ diff --git a/packages/kbn-pm/src/utils/__fixtures__/kibana/packages/bar/package.json b/packages/kbn-pm/src/utils/__fixtures__/kibana/packages/bar/package.json index b5eae58393860..06a8b8dcc6aa8 100644 --- a/packages/kbn-pm/src/utils/__fixtures__/kibana/packages/bar/package.json +++ b/packages/kbn-pm/src/utils/__fixtures__/kibana/packages/bar/package.json @@ -1,7 +1,4 @@ { "name": "bar", - "version": "1.0.0", - "dependencies": { - "foo": "link:../foo" - } + "version": "1.0.0" } diff --git a/packages/kbn-pm/src/utils/__fixtures__/plugins/quux/package.json b/packages/kbn-pm/src/utils/__fixtures__/plugins/quux/package.json index b2794986c5b0b..f2a3062454509 100644 --- a/packages/kbn-pm/src/utils/__fixtures__/plugins/quux/package.json +++ b/packages/kbn-pm/src/utils/__fixtures__/plugins/quux/package.json @@ -1,8 +1,4 @@ { "name": "quux", - "version": "1.0.0", - "dependencies": { - "bar": "link:../../kibana/packages/bar", - "baz": "link:../baz" - } + "version": "1.0.0" } diff --git a/packages/kbn-pm/src/utils/__fixtures__/plugins/zorge/package.json b/packages/kbn-pm/src/utils/__fixtures__/plugins/zorge/package.json index 80a27b17661dd..3f22a1845b66a 100644 --- a/packages/kbn-pm/src/utils/__fixtures__/plugins/zorge/package.json +++ b/packages/kbn-pm/src/utils/__fixtures__/plugins/zorge/package.json @@ -1,7 +1,4 @@ { "name": "zorge", - "version": "1.0.0", - "dependencies": { - "foo": "link:../../kibana/packages/foo" - } + "version": "1.0.0" } diff --git a/packages/kbn-pm/src/utils/__snapshots__/project.test.ts.snap b/packages/kbn-pm/src/utils/__snapshots__/project.test.ts.snap index b3bcc402db2a3..d4a4e5ca23452 100644 --- a/packages/kbn-pm/src/utils/__snapshots__/project.test.ts.snap +++ b/packages/kbn-pm/src/utils/__snapshots__/project.test.ts.snap @@ -1,7 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#ensureValidProjectDependency using link:, but with wrong path 1`] = `"[kibana] depends on [foo] using 'link:', but the path is wrong. Update its package.json to the expected value below."`; - -exports[`#ensureValidProjectDependency using version instead of link: 1`] = `"[kibana] depends on [foo] but it's not using the local package. Update its package.json to the expected value below."`; - exports[`#getExecutables() throws CliError when bin is something strange 1`] = `"[kibana] has an invalid \\"bin\\" field in its package.json, expected an object or a string"`; diff --git a/packages/kbn-pm/src/utils/__snapshots__/projects.test.ts.snap b/packages/kbn-pm/src/utils/__snapshots__/projects.test.ts.snap index 86ba136c50aa1..a716b9fab4e5b 100644 --- a/packages/kbn-pm/src/utils/__snapshots__/projects.test.ts.snap +++ b/packages/kbn-pm/src/utils/__snapshots__/projects.test.ts.snap @@ -2,37 +2,28 @@ exports[`#buildProjectGraph builds full project graph 1`] = ` Object { - "bar": Array [ - "foo", - ], + "bar": Array [], "baz": Array [], "foo": Array [], "kibana": Array [ "foo", ], - "quux": Array [ - "bar", - "baz", - ], - "zorge": Array [ - "foo", - ], + "quux": Array [], + "zorge": Array [], } `; exports[`#topologicallyBatchProjects batches projects topologically based on their project dependencies 1`] = ` Array [ Array [ + "bar", "foo", "baz", - ], - Array [ - "kibana", - "bar", + "quux", "zorge", ], Array [ - "quux", + "kibana", ], ] `; @@ -43,10 +34,8 @@ Array [ "kibana", "bar", "baz", - "zorge", - ], - Array [ "quux", + "zorge", ], ] `; diff --git a/packages/kbn-pm/src/utils/project.test.ts b/packages/kbn-pm/src/utils/project.test.ts index 9be5953880283..389dbf123cd52 100644 --- a/packages/kbn-pm/src/utils/project.test.ts +++ b/packages/kbn-pm/src/utils/project.test.ts @@ -50,65 +50,6 @@ test('fields', async () => { expect(kibana.hasScript('build')).toBe(false); }); -describe('#ensureValidProjectDependency', () => { - test('valid link: version', async () => { - const root = createProjectWith({ - dependencies: { - foo: 'link:packages/foo', - }, - }); - - const foo = createProjectWith( - { - name: 'foo', - }, - 'packages/foo' - ); - - expect(() => root.ensureValidProjectDependency(foo)).not.toThrow(); - }); - - test('using link:, but with wrong path', () => { - const root = createProjectWith( - { - dependencies: { - foo: 'link:wrong/path', - }, - }, - rootPath - ); - - const foo = createProjectWith( - { - name: 'foo', - }, - 'packages/foo' - ); - - expect(() => root.ensureValidProjectDependency(foo)).toThrowErrorMatchingSnapshot(); - }); - - test('using version instead of link:', () => { - const root = createProjectWith( - { - dependencies: { - foo: '1.0.0', - }, - }, - rootPath - ); - - const foo = createProjectWith( - { - name: 'foo', - }, - 'packages/foo' - ); - - expect(() => root.ensureValidProjectDependency(foo)).toThrowErrorMatchingSnapshot(); - }); -}); - describe('#getExecutables()', () => { test('converts bin:string to an object with absolute paths', () => { const project = createProjectWith({ diff --git a/packages/kbn-pm/src/utils/project.ts b/packages/kbn-pm/src/utils/project.ts index 48c606c10da42..842f828543116 100644 --- a/packages/kbn-pm/src/utils/project.ts +++ b/packages/kbn-pm/src/utils/project.ts @@ -88,48 +88,6 @@ export class Project { return this.json.name; } - public ensureValidProjectDependency(project: Project) { - const relativePathToProject = normalizePath(Path.relative(this.path, project.path)); - const relativePathToProjectIfBazelPkg = normalizePath( - Path.relative( - this.path, - `${__dirname}/../../../bazel-bin/packages/${Path.basename(project.path)}` - ) - ); - - const versionInPackageJson = this.allDependencies[project.name]; - const expectedVersionInPackageJson = `link:${relativePathToProject}`; - const expectedVersionInPackageJsonIfBazelPkg = `link:${relativePathToProjectIfBazelPkg}`; - - // TODO: after introduce bazel to build all the packages and completely remove the support for kbn packages - // do not allow child projects to hold dependencies, unless they are meant to be published externally - if ( - versionInPackageJson === expectedVersionInPackageJson || - versionInPackageJson === expectedVersionInPackageJsonIfBazelPkg - ) { - return; - } - - const updateMsg = 'Update its package.json to the expected value below.'; - const meta = { - actual: `"${project.name}": "${versionInPackageJson}"`, - expected: `"${project.name}": "${expectedVersionInPackageJson}" or "${project.name}": "${expectedVersionInPackageJsonIfBazelPkg}"`, - package: `${this.name} (${this.packageJsonLocation})`, - }; - - if (isLinkDependency(versionInPackageJson)) { - throw new CliError( - `[${this.name}] depends on [${project.name}] using 'link:', but the path is wrong. ${updateMsg}`, - meta - ); - } - - throw new CliError( - `[${this.name}] depends on [${project.name}] but it's not using the local package. ${updateMsg}`, - meta - ); - } - public getBuildConfig(): BuildConfig { return (this.json.kibana && this.json.kibana.build) || {}; } @@ -206,8 +164,3 @@ export class Project { return Object.values(this.allDependencies).every((dep) => isLinkDependency(dep)); } } - -// We normalize all path separators to `/` in generated files -function normalizePath(path: string) { - return path.replace(/[\\\/]+/g, '/'); -} diff --git a/packages/kbn-pm/src/utils/projects.test.ts b/packages/kbn-pm/src/utils/projects.test.ts index bf7bb052b254a..c87876642cf0b 100644 --- a/packages/kbn-pm/src/utils/projects.test.ts +++ b/packages/kbn-pm/src/utils/projects.test.ts @@ -249,6 +249,6 @@ describe('#includeTransitiveProjects', () => { const quux = projects.get('quux')!; const withTransitive = includeTransitiveProjects([quux], projects); - expect([...withTransitive.keys()]).toEqual(['quux', 'bar', 'baz', 'foo']); + expect([...withTransitive.keys()]).toEqual(['quux']); }); }); diff --git a/packages/kbn-pm/src/utils/projects.ts b/packages/kbn-pm/src/utils/projects.ts index 28a1fcfec8c36..e30dfc9f4c876 100644 --- a/packages/kbn-pm/src/utils/projects.ts +++ b/packages/kbn-pm/src/utils/projects.ts @@ -11,6 +11,7 @@ import path from 'path'; import { promisify } from 'util'; import { CliError } from './errors'; +import { log } from './log'; import { Project } from './project'; const glob = promisify(globSync); @@ -115,14 +116,23 @@ export function buildProjectGraph(projects: ProjectMap) { const projectGraph: ProjectGraph = new Map(); for (const project of projects.values()) { - const projectDeps = []; + const projectDeps: Project[] = []; const dependencies = project.allDependencies; + if (!project.isSinglePackageJsonProject && Object.keys(dependencies).length > 0) { + log.warning( + `${project.name} is not allowed to hold local dependencies and they will be discarded. Please declare them at the root package.json` + ); + } + + if (!project.isSinglePackageJsonProject) { + projectGraph.set(project.name, projectDeps); + continue; + } + for (const depName of Object.keys(dependencies)) { if (projects.has(depName)) { const dep = projects.get(depName)!; - project.ensureValidProjectDependency(dep); - projectDeps.push(dep); } } diff --git a/packages/kbn-rule-data-utils/BUILD.bazel b/packages/kbn-rule-data-utils/BUILD.bazel index 6477b558db9cb..d4dc6577c02b4 100644 --- a/packages/kbn-rule-data-utils/BUILD.bazel +++ b/packages/kbn-rule-data-utils/BUILD.bazel @@ -44,6 +44,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -69,7 +76,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-rule-data-utils/package.json b/packages/kbn-rule-data-utils/package.json index 9372d7e70a8d1..d11f65e294a48 100644 --- a/packages/kbn-rule-data-utils/package.json +++ b/packages/kbn-rule-data-utils/package.json @@ -1,5 +1,6 @@ { "name": "@kbn/rule-data-utils", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", diff --git a/packages/kbn-securitysolution-rules/BUILD.bazel b/packages/kbn-securitysolution-rules/BUILD.bazel index 80a27a426fbb2..31b8fa8679312 100644 --- a/packages/kbn-securitysolution-rules/BUILD.bazel +++ b/packages/kbn-securitysolution-rules/BUILD.bazel @@ -47,6 +47,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -71,7 +78,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-rules/package.json b/packages/kbn-securitysolution-rules/package.json index 4962576450f59..da061b244e7a0 100644 --- a/packages/kbn-securitysolution-rules/package.json +++ b/packages/kbn-securitysolution-rules/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "security solution rule utilities to use across plugins", "license": "SSPL-1.0 OR Elastic License 2.0", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "private": true } diff --git a/packages/kbn-securitysolution-utils/BUILD.bazel b/packages/kbn-securitysolution-utils/BUILD.bazel index 70ecc2712d4af..1842e5d1a523f 100644 --- a/packages/kbn-securitysolution-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-utils/BUILD.bazel @@ -47,6 +47,13 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -71,7 +78,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-securitysolution-utils/package.json b/packages/kbn-securitysolution-utils/package.json index 8f347972f8316..e43d2570f730f 100644 --- a/packages/kbn-securitysolution-utils/package.json +++ b/packages/kbn-securitysolution-utils/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "security solution utilities to use across plugins such lists, security_solution, cases, etc...", "license": "SSPL-1.0 OR Elastic License 2.0", + "browser": "./target_web/index.js", "main": "./target_node/index.js", "private": true } diff --git a/packages/kbn-server-route-repository/BUILD.bazel b/packages/kbn-server-route-repository/BUILD.bazel index 06c09260e2fa6..b635362ae1521 100644 --- a/packages/kbn-server-route-repository/BUILD.bazel +++ b/packages/kbn-server-route-repository/BUILD.bazel @@ -52,6 +52,17 @@ jsts_transpiler( build_pkg_name = package_name(), ) +jsts_transpiler( + name = "target_web", + srcs = [ + "src/web_index.ts", + "src/format_request.ts", + "src/parse_endpoint.ts", + ], + build_pkg_name = package_name(), + web = True, +) + ts_config( name = "tsconfig", src = "tsconfig.json", @@ -76,7 +87,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node"], + deps = RUNTIME_DEPS + [":target_node", ":target_web"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-server-route-repository/README.md b/packages/kbn-server-route-repository/README.md index e22205540ef31..13d7972028cb7 100644 --- a/packages/kbn-server-route-repository/README.md +++ b/packages/kbn-server-route-repository/README.md @@ -5,3 +5,11 @@ Utility functions for creating a typed server route repository, and a typed clie ## Usage TBD + +## Server vs. Browser entry points + +This package exposes utils that can be used on both: the server and the browser. +However, importing the package might bring in server-only code, affecting the bundle size. +To avoid this, the package exposes 2 entry points: [`index.js`](./src/index.ts) and [`web_index.js`](./src/web_index.ts). + +When adding utilities to this package, please make sure to update the entry points accordingly and the [BUILD.bazel](./BUILD.bazel)'s `target_web` target build to include all the necessary files. diff --git a/packages/kbn-server-route-repository/package.json b/packages/kbn-server-route-repository/package.json index 32e59c896db00..1491f24c54dc1 100644 --- a/packages/kbn-server-route-repository/package.json +++ b/packages/kbn-server-route-repository/package.json @@ -1,5 +1,6 @@ { "name": "@kbn/server-route-repository", + "browser": "./target_web/web_index.js", "main": "./target_node/index.js", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", diff --git a/packages/kbn-server-route-repository/src/web_index.ts b/packages/kbn-server-route-repository/src/web_index.ts new file mode 100644 index 0000000000000..3ceeed55236bd --- /dev/null +++ b/packages/kbn-server-route-repository/src/web_index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { formatRequest } from './format_request'; +export { parseEndpoint } from './parse_endpoint'; +export type { + RouteRepositoryClient, + ReturnOf, + EndpointOf, + ClientRequestParamsOf, + DecodedRequestParamsOf, + ServerRouteRepository, + ServerRoute, + RouteParamsRT, + RouteState, +} from './typings'; diff --git a/packages/kbn-stdio-dev-helpers/src/observe_lines.ts b/packages/kbn-stdio-dev-helpers/src/observe_lines.ts index 9b6ba85fea897..4b3a2ac2287da 100644 --- a/packages/kbn-stdio-dev-helpers/src/observe_lines.ts +++ b/packages/kbn-stdio-dev-helpers/src/observe_lines.ts @@ -9,7 +9,6 @@ import { Readable } from 'stream'; import * as Rx from 'rxjs'; -import { scan, takeUntil, share, materialize, mergeMap, last, catchError } from 'rxjs/operators'; const SEP = /\r?\n/; @@ -25,13 +24,13 @@ import { observeReadable } from './observe_readable'; * @return {Rx.Observable} */ export function observeLines(readable: Readable): Rx.Observable { - const done$ = observeReadable(readable).pipe(share()); + const done$ = observeReadable(readable).pipe(Rx.share()); const scan$: Rx.Observable<{ buffer: string; lines?: string[] }> = Rx.fromEvent( readable, 'data' ).pipe( - scan( + Rx.scan( ({ buffer }, chunk) => { buffer += chunk; @@ -53,9 +52,9 @@ export function observeLines(readable: Readable): Rx.Observable { ), // stop if done completes or errors - takeUntil(done$.pipe(materialize())), + Rx.takeUntil(done$.pipe(Rx.materialize())), - share() + Rx.share() ); return Rx.merge( @@ -63,14 +62,12 @@ export function observeLines(readable: Readable): Rx.Observable { done$, // merge in the "lines" from each step - scan$.pipe(mergeMap(({ lines }) => lines || [])), + scan$.pipe(Rx.mergeMap(({ lines }) => lines || [])), // inject the "unsplit" data at the end scan$.pipe( - last(), - mergeMap(({ buffer }) => (buffer ? [buffer] : [])), - // if there were no lines, last() will error, so catch and complete - catchError(() => Rx.empty()) + Rx.takeLast(1), + Rx.mergeMap(({ buffer }) => (buffer ? [buffer] : [])) ) ); } diff --git a/packages/kbn-stdio-dev-helpers/src/observe_readable.ts b/packages/kbn-stdio-dev-helpers/src/observe_readable.ts index 29aa77a538601..fa087c299aa51 100644 --- a/packages/kbn-stdio-dev-helpers/src/observe_readable.ts +++ b/packages/kbn-stdio-dev-helpers/src/observe_readable.ts @@ -9,7 +9,6 @@ import { Readable } from 'stream'; import * as Rx from 'rxjs'; -import { first, ignoreElements, mergeMap } from 'rxjs/operators'; /** * Produces an Observable from a ReadableSteam that: @@ -18,11 +17,12 @@ import { first, ignoreElements, mergeMap } from 'rxjs/operators'; */ export function observeReadable(readable: Readable): Rx.Observable { return Rx.race( - Rx.fromEvent(readable, 'end').pipe(first(), ignoreElements()), - + Rx.fromEvent(readable, 'end').pipe(Rx.first(), Rx.ignoreElements()), Rx.fromEvent(readable, 'error').pipe( - first(), - mergeMap((err) => Rx.throwError(err)) + Rx.first(), + Rx.map((err) => { + throw err; + }) ) ); } diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index 8a7aabdecd61e..f7599e6d81649 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -64,6 +64,7 @@ RUNTIME_DEPS = [ "@npm//jest-snapshot", "@npm//jest-styled-components", "@npm//joi", + "@npm//js-yaml", "@npm//mustache", "@npm//normalize-path", "@npm//parse-link-header", @@ -108,6 +109,7 @@ TYPES_DEPS = [ "@npm//@types/he", "@npm//@types/history", "@npm//@types/jest", + "@npm//@types/js-yaml", "@npm//@types/joi", "@npm//@types/lodash", "@npm//@types/mustache", diff --git a/packages/kbn-test/README.md b/packages/kbn-test/README.md index 3159e5c2492b4..72fb5c3358ce7 100644 --- a/packages/kbn-test/README.md +++ b/packages/kbn-test/README.md @@ -15,14 +15,14 @@ Functional testing methods exist in the `src/functional_tests` directory. They d #### runTests(configPaths: Array) For each config file specified in configPaths, starts Elasticsearch and Kibana once, runs tests specified in that config file, and shuts down Elasticsearch and Kibana once completed. (Repeats for every config file.) -`configPaths`: array of strings, each an absolute path to a config file that looks like [this](../../test/functional/config.js), following the config schema specified [here](../../src/functional_test_runner/lib/config/schema.js). +`configPaths`: array of strings, each an absolute path to a config file that looks like [this](../../test/functional/config.base.js), following the config schema specified [here](../../src/functional_test_runner/lib/config/schema.js). Internally the method that starts Elasticsearch comes from [kbn-es](../../packages/kbn-es). #### startServers(configPath: string) Starts Elasticsearch and Kibana servers given a specified config. -`configPath`: absolute path to a config file that looks like [this](../../test/functional/config.js), following the config schema specified [here](../../src/functional_test_runner/lib/config/schema.js). +`configPath`: absolute path to a config file that looks like [this](../../test/functional/config.base.js), following the config schema specified [here](../../src/functional_test_runner/lib/config/schema.js). Allows users to start another process to run just the tests while keeping the servers running with this method. Start servers _and_ run tests using the same config file ([see how](../../scripts/README.md)). diff --git a/packages/kbn-test/src/es/test_es_cluster.ts b/packages/kbn-test/src/es/test_es_cluster.ts index a12b1d852ced9..42dc19445c293 100644 --- a/packages/kbn-test/src/es/test_es_cluster.ts +++ b/packages/kbn-test/src/es/test_es_cluster.ts @@ -41,6 +41,7 @@ interface Node { ) => Promise<{ insallPath: string }>; start: (installPath: string, opts: Record) => Promise; stop: () => Promise; + kill: () => Promise; } export interface ICluster { @@ -268,20 +269,26 @@ export function createTestEsCluster< } async stop() { - const nodeStopPromises = []; - for (let i = 0; i < this.nodes.length; i++) { - nodeStopPromises.push(async () => { + await Promise.all( + this.nodes.map(async (node, i) => { log.info(`[es] stopping node ${nodes[i].name}`); - return await this.nodes[i].stop(); - }); - } - await Promise.all(nodeStopPromises.map(async (stop) => await stop())); + await node.stop(); + }) + ); log.info('[es] stopped'); } async cleanup() { - await this.stop(); + log.info('[es] killing', this.nodes.length === 1 ? 'node' : `${this.nodes.length} nodes`); + await Promise.all( + this.nodes.map(async (node, i) => { + log.info(`[es] stopping node ${nodes[i].name}`); + // we are deleting this install, stop ES more aggressively + await node.kill(); + }) + ); + await del(config.installPath, { force: true }); log.info('[es] cleanup complete'); } diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index ee01b0ccfde9c..4159533e628bc 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -98,11 +98,7 @@ export function runFtrCli() { }); } - try { - await functionalTestRunner.close(); - } finally { - process.exit(); - } + process.exit(); }; process.on('unhandledRejection', (err) => diff --git a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts index 1ba99fc69a5d3..0ceba511f9b9b 100644 --- a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts +++ b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts @@ -14,10 +14,9 @@ import { REPO_ROOT } from '@kbn/utils'; import { Suite, Test } from './fake_mocha_types'; import { Lifecycle, - LifecyclePhase, - TestMetadata, readConfigFile, ProviderCollection, + Providers, readProviderSpec, setupMocha, runTests, @@ -29,10 +28,6 @@ import { import { createEsClientForFtrConfig } from '../es'; export class FunctionalTestRunner { - public readonly lifecycle = new Lifecycle(); - public readonly testMetadata = new TestMetadata(this.lifecycle); - private closed = false; - private readonly esVersion: EsVersion; constructor( private readonly log: ToolingLog, @@ -40,12 +35,6 @@ export class FunctionalTestRunner { private readonly configOverrides: any, esVersion?: string | EsVersion ) { - for (const [key, value] of Object.entries(this.lifecycle)) { - if (value instanceof LifecyclePhase) { - value.before$.subscribe(() => log.verbose('starting %j lifecycle phase', key)); - value.after$.subscribe(() => log.verbose('starting %j lifecycle phase', key)); - } - } this.esVersion = esVersion === undefined ? EsVersion.getDefault() @@ -55,19 +44,28 @@ export class FunctionalTestRunner { } async run() { - return await this._run(async (config, coreProviders) => { - SuiteTracker.startTracking(this.lifecycle, this.configFile); + const testStats = await this.getTestStats(); + + return await this.runHarness(async (config, lifecycle, coreProviders) => { + SuiteTracker.startTracking(lifecycle, this.configFile); + + const realServices = + !testStats || (testStats.testCount > 0 && testStats.nonSkippedTestCount > 0); - const providers = new ProviderCollection(this.log, [ - ...coreProviders, - ...readProviderSpec('Service', config.get('services')), - ...readProviderSpec('PageObject', config.get('pageObjects')), - ]); + const providers = realServices + ? new ProviderCollection(this.log, [ + ...coreProviders, + ...readProviderSpec('Service', config.get('services')), + ...readProviderSpec('PageObject', config.get('pageObjects')), + ]) + : this.getStubProviderCollection(config, coreProviders); - if (providers.hasService('es')) { - await this.validateEsVersion(config); + if (realServices) { + if (providers.hasService('es')) { + await this.validateEsVersion(config); + } + await providers.loadAll(); } - await providers.loadAll(); const customTestRunner = config.get('testRunner'); if (customTestRunner) { @@ -90,7 +88,7 @@ export class FunctionalTestRunner { } const mocha = await setupMocha( - this.lifecycle, + lifecycle, this.log, config, providers, @@ -108,10 +106,10 @@ export class FunctionalTestRunner { return this.simulateMochaDryRun(mocha); } - await this.lifecycle.beforeTests.trigger(mocha.suite); + await lifecycle.beforeTests.trigger(mocha.suite); this.log.info('Starting tests'); - return await runTests(this.lifecycle, mocha); + return await runTests(lifecycle, mocha); }); } @@ -143,60 +141,73 @@ export class FunctionalTestRunner { } async getTestStats() { - return await this._run(async (config, coreProviders) => { + return await this.runHarness(async (config, lifecycle, coreProviders) => { if (config.get('testRunner')) { - throw new Error('Unable to get test stats for config that uses a custom test runner'); + return; } - // replace the function of custom service providers so that they return - // promise-like objects which never resolve, essentially disabling them - // allowing us to load the test files and populate the mocha suites - const readStubbedProviderSpec = (type: string, providers: any, skip: string[]) => - readProviderSpec(type, providers).map((p) => ({ - ...p, - fn: skip.includes(p.name) - ? (ctx: any) => { - const result = ProviderCollection.callProviderFn(p.fn, ctx); - - if ('then' in result) { - throw new Error( - `Provider [${p.name}] returns a promise so it can't loaded during test analysis` - ); - } - - return result; - } - : () => ({ - then: () => {}, - }), - })); - - const providers = new ProviderCollection(this.log, [ - ...coreProviders, - ...readStubbedProviderSpec( - 'Service', - config.get('services'), - config.get('servicesRequiredForTestAnalysis') - ), - ...readStubbedProviderSpec('PageObject', config.get('pageObjects'), []), - ]); - - const mocha = await setupMocha(this.lifecycle, this.log, config, providers, this.esVersion); - - const countTests = (suite: Suite): number => - suite.suites.reduce((sum, s) => sum + countTests(s), suite.tests.length); + const providers = this.getStubProviderCollection(config, coreProviders); + const mocha = await setupMocha(lifecycle, this.log, config, providers, this.esVersion); + + const queue = new Set([mocha.suite]); + const allTests: Test[] = []; + for (const suite of queue) { + for (const test of suite.tests) { + allTests.push(test); + } + for (const childSuite of suite.suites) { + queue.add(childSuite); + } + } return { - testCount: countTests(mocha.suite), + testCount: allTests.length, + nonSkippedTestCount: allTests.filter((t) => !t.pending).length, testsExcludedByTag: mocha.testsExcludedByTag.map((t: Test) => t.fullTitle()), }; }); } - async _run( - handler: (config: Config, coreProvider: ReturnType) => Promise + private getStubProviderCollection(config: Config, coreProviders: Providers) { + // when we want to load the tests but not actually run anything we can + // use stubbed providers which allow mocha to do it's thing without taking + // too much time + const readStubbedProviderSpec = (type: string, providers: any, skip: string[]) => + readProviderSpec(type, providers).map((p) => ({ + ...p, + fn: skip.includes(p.name) + ? (ctx: any) => { + const result = ProviderCollection.callProviderFn(p.fn, ctx); + + if ('then' in result) { + throw new Error( + `Provider [${p.name}] returns a promise so it can't loaded during test analysis` + ); + } + + return result; + } + : () => ({ + then: () => {}, + }), + })); + + return new ProviderCollection(this.log, [ + ...coreProviders, + ...readStubbedProviderSpec( + 'Service', + config.get('services'), + config.get('servicesRequiredForTestAnalysis') + ), + ...readStubbedProviderSpec('PageObject', config.get('pageObjects'), []), + ]); + } + + private async runHarness( + handler: (config: Config, lifecycle: Lifecycle, coreProviders: Providers) => Promise ): Promise { let runErrorOccurred = false; + const lifecycle = new Lifecycle(this.log); try { const config = await readConfigFile( @@ -205,7 +216,7 @@ export class FunctionalTestRunner { this.configFile, this.configOverrides ); - this.log.info('Config loaded'); + this.log.debug('Config loaded'); if ( (!config.get('testFiles') || config.get('testFiles').length === 0) && @@ -217,26 +228,25 @@ export class FunctionalTestRunner { const dockerServers = new DockerServersService( config.get('dockerServers'), this.log, - this.lifecycle + lifecycle ); // base level services that functional_test_runner exposes const coreProviders = readProviderSpec('Service', { - lifecycle: () => this.lifecycle, + lifecycle: () => lifecycle, log: () => this.log, - testMetadata: () => this.testMetadata, config: () => config, dockerServers: () => dockerServers, esVersion: () => this.esVersion, }); - return await handler(config, coreProviders); + return await handler(config, lifecycle, coreProviders); } catch (runError) { runErrorOccurred = true; throw runError; } finally { try { - await this.close(); + await lifecycle.cleanup.trigger(); } catch (closeError) { if (runErrorOccurred) { this.log.error('failed to close functional_test_runner'); @@ -249,13 +259,6 @@ export class FunctionalTestRunner { } } - async close() { - if (this.closed) return; - - this.closed = true; - await this.lifecycle.cleanup.trigger(); - } - simulateMochaDryRun(mocha: any) { interface TestEntry { file: string; diff --git a/packages/kbn-test/src/functional_test_runner/index.ts b/packages/kbn-test/src/functional_test_runner/index.ts index b5d55c28ee9b2..1a8efb6097048 100644 --- a/packages/kbn-test/src/functional_test_runner/index.ts +++ b/packages/kbn-test/src/functional_test_runner/index.ts @@ -15,7 +15,6 @@ export { Lifecycle, LifecyclePhase, } from './lib'; -export type { ScreenshotRecord } from './lib'; export { runFtrCli } from './cli'; export * from './lib/docker_servers'; export * from './public_types'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/ftr_configs_manifest.ts b/packages/kbn-test/src/functional_test_runner/lib/config/ftr_configs_manifest.ts new file mode 100644 index 0000000000000..93cab4bfaac95 --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/config/ftr_configs_manifest.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import Fs from 'fs'; + +import { REPO_ROOT } from '@kbn/utils'; +import JsYaml from 'js-yaml'; + +export const FTR_CONFIGS_MANIFEST_REL = '.buildkite/ftr_configs.yml'; + +const ftrConfigsManifest = JsYaml.safeLoad( + Fs.readFileSync(Path.resolve(REPO_ROOT, FTR_CONFIGS_MANIFEST_REL), 'utf8') +); + +export const FTR_CONFIGS_MANIFEST_PATHS = (Object.values(ftrConfigsManifest) as string[][]) + .flat() + .map((rel) => Path.resolve(REPO_ROOT, rel)); diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.js b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.ts similarity index 70% rename from packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.js rename to packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.ts index d1ce17cc95b7b..29b723dae7195 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.js +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.ts @@ -14,36 +14,35 @@ import { EsVersion } from '../es_version'; const log = new ToolingLog(); const esVersion = new EsVersion('8.0.0'); +const CONFIG_PATH_1 = require.resolve('./__fixtures__/config.1.js'); +const CONFIG_PATH_2 = require.resolve('./__fixtures__/config.2.js'); +const CONFIG_PATH_INVALID = require.resolve('./__fixtures__/config.invalid.js'); + describe('readConfigFile()', () => { it('reads config from a file, returns an instance of Config class', async () => { - const config = await readConfigFile(log, esVersion, require.resolve('./__fixtures__/config.1')); + const config = await readConfigFile(log, esVersion, CONFIG_PATH_1); expect(config instanceof Config).toBeTruthy(); expect(config.get('testFiles')).toEqual(['config.1']); }); it('merges setting overrides into log', async () => { - const config = await readConfigFile( - log, - esVersion, - require.resolve('./__fixtures__/config.1'), - { - screenshots: { - directory: 'foo.bar', - }, - } - ); + const config = await readConfigFile(log, esVersion, CONFIG_PATH_1, { + screenshots: { + directory: 'foo.bar', + }, + }); expect(config.get('screenshots.directory')).toBe('foo.bar'); }); it('supports loading config files from within config files', async () => { - const config = await readConfigFile(log, esVersion, require.resolve('./__fixtures__/config.2')); + const config = await readConfigFile(log, esVersion, CONFIG_PATH_2); expect(config.get('testFiles')).toEqual(['config.1', 'config.2']); }); it('throws if settings are invalid', async () => { try { - await readConfigFile(log, esVersion, require.resolve('./__fixtures__/config.invalid')); + await readConfigFile(log, esVersion, CONFIG_PATH_INVALID); throw new Error('expected readConfigFile() to fail'); } catch (err) { expect(err.message).toMatch(/"foo"/); diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts index d026842f3a4f1..24702d699064c 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts @@ -6,25 +6,41 @@ * Side Public License, v 1. */ +import Path from 'path'; import { ToolingLog } from '@kbn/tooling-log'; import { defaultsDeep } from 'lodash'; +import { createFlagError } from '@kbn/dev-utils'; import { Config } from './config'; import { EsVersion } from '../es_version'; +import { FTR_CONFIGS_MANIFEST_REL, FTR_CONFIGS_MANIFEST_PATHS } from './ftr_configs_manifest'; const cache = new WeakMap(); async function getSettingsFromFile( log: ToolingLog, esVersion: EsVersion, - path: string, - settingOverrides: any + options: { + path: string; + settingOverrides: any; + primary: boolean; + } ) { - const configModule = require(path); // eslint-disable-line @typescript-eslint/no-var-requires + if ( + options.primary && + !FTR_CONFIGS_MANIFEST_PATHS.includes(options.path) && + !options.path.includes(`${Path.sep}__fixtures__${Path.sep}`) + ) { + throw createFlagError( + `Refusing to load FTR Config which is not listed in [${FTR_CONFIGS_MANIFEST_REL}]. All FTR Config files must be listed there, use the "enabled" key if the FTR Config should be run on automatically on PR CI, or the "disabled" key if it is run manually or by a special job.` + ); + } + + const configModule = require(options.path); // eslint-disable-line @typescript-eslint/no-var-requires const configProvider = configModule.__esModule ? configModule.default : configModule; if (!cache.has(configProvider)) { - log.debug('Loading config file from %j', path); + log.debug('Loading config file from %j', options.path); cache.set( configProvider, configProvider({ @@ -32,7 +48,11 @@ async function getSettingsFromFile( esVersion, async readConfigFile(p: string, o: any) { return new Config({ - settings: await getSettingsFromFile(log, esVersion, p, o), + settings: await getSettingsFromFile(log, esVersion, { + path: p, + settingOverrides: o, + primary: false, + }), primary: false, path: p, }); @@ -43,7 +63,7 @@ async function getSettingsFromFile( const settingsWithDefaults: any = defaultsDeep( {}, - settingOverrides, + options.settingOverrides, await cache.get(configProvider)! ); @@ -57,7 +77,11 @@ export async function readConfigFile( settingOverrides: any = {} ) { return new Config({ - settings: await getSettingsFromFile(log, esVersion, path, settingOverrides), + settings: await getSettingsFromFile(log, esVersion, { + path, + settingOverrides, + primary: true, + }), primary: true, path, }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 17c3af046f92f..d2182064d352e 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -226,6 +226,11 @@ export const schema = Joi.object() wait: Joi.object() .regex() .default(/Kibana is now available/), + + /** + * Does this test config only work when run against source? + */ + alwaysUseSource: Joi.boolean().default(false), }) .default(), env: Joi.object().unknown().default(), diff --git a/packages/kbn-test/src/functional_test_runner/lib/index.ts b/packages/kbn-test/src/functional_test_runner/lib/index.ts index 077a62e8e74e5..9f637f8bd5b4f 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/index.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/index.ts @@ -12,7 +12,6 @@ export { readConfigFile, Config } from './config'; export * from './providers'; // @internal export { runTests, setupMocha } from './mocha'; -export * from './test_metadata'; export * from './docker_servers'; export { SuiteTracker } from './suite_tracker'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts b/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts index e683ec23a8d84..230eacb91008e 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts @@ -6,29 +6,51 @@ * Side Public License, v 1. */ +import * as Rx from 'rxjs'; +import { ToolingLog } from '@kbn/tooling-log'; + import { LifecyclePhase } from './lifecycle_phase'; import { Suite, Test } from '../fake_mocha_types'; export class Lifecycle { + /** root subscription to cleanup lifecycle phases when lifecycle completes */ + private readonly sub = new Rx.Subscription(); + /** lifecycle phase that will run handlers once before tests execute */ - public readonly beforeTests = new LifecyclePhase<[Suite]>({ + public readonly beforeTests = new LifecyclePhase<[Suite]>(this.sub, { singular: true, }); /** lifecycle phase that runs handlers before each runnable (test and hooks) */ - public readonly beforeEachRunnable = new LifecyclePhase<[Test]>(); + public readonly beforeEachRunnable = new LifecyclePhase<[Test]>(this.sub); /** lifecycle phase that runs handlers before each suite */ - public readonly beforeTestSuite = new LifecyclePhase<[Suite]>(); + public readonly beforeTestSuite = new LifecyclePhase<[Suite]>(this.sub); /** lifecycle phase that runs handlers before each test */ - public readonly beforeEachTest = new LifecyclePhase<[Test]>(); + public readonly beforeEachTest = new LifecyclePhase<[Test]>(this.sub); /** lifecycle phase that runs handlers after each suite */ - public readonly afterTestSuite = new LifecyclePhase<[Suite]>(); + public readonly afterTestSuite = new LifecyclePhase<[Suite]>(this.sub); /** lifecycle phase that runs handlers after a test fails */ - public readonly testFailure = new LifecyclePhase<[Error, Test]>(); + public readonly testFailure = new LifecyclePhase<[Error, Test]>(this.sub); /** lifecycle phase that runs handlers after a hook fails */ - public readonly testHookFailure = new LifecyclePhase<[Error, Test]>(); + public readonly testHookFailure = new LifecyclePhase<[Error, Test]>(this.sub); /** lifecycle phase that runs handlers at the very end of execution */ - public readonly cleanup = new LifecyclePhase<[]>({ + public readonly cleanup = new LifecyclePhase<[]>(this.sub, { singular: true, }); + + constructor(log: ToolingLog) { + for (const [name, phase] of Object.entries(this)) { + if (phase instanceof LifecyclePhase) { + phase.before$.subscribe(() => log.verbose('starting %j lifecycle phase', name)); + phase.after$.subscribe(() => log.verbose('starting %j lifecycle phase', name)); + } + } + + // after the singular cleanup lifecycle phase completes unsubscribe from the root subscription + this.cleanup.after$.pipe(Rx.materialize()).subscribe((n) => { + if (n.kind === 'C') { + this.sub.unsubscribe(); + } + }); + } } diff --git a/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.test.ts b/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.test.ts index 503a9490f2664..47ab24169d204 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.test.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.test.ts @@ -26,7 +26,7 @@ describe('with randomness', () => { }); it('calls handlers in random order', async () => { - const phase = new LifecyclePhase(); + const phase = new LifecyclePhase(new Rx.Subscription()); const order: string[] = []; phase.add( @@ -69,7 +69,7 @@ describe('without randomness', () => { afterEach(() => jest.restoreAllMocks()); it('calls all handlers and throws first error', async () => { - const phase = new LifecyclePhase(); + const phase = new LifecyclePhase(new Rx.Subscription()); const fn1 = jest.fn(); phase.add(fn1); @@ -88,7 +88,7 @@ describe('without randomness', () => { }); it('triggers before$ just before calling handler and after$ once it resolves', async () => { - const phase = new LifecyclePhase(); + const phase = new LifecyclePhase(new Rx.Subscription()); const order: string[] = []; const beforeSub = jest.fn(() => order.push('before')); @@ -116,7 +116,7 @@ describe('without randomness', () => { }); it('completes before$ and after$ if phase is singular', async () => { - const phase = new LifecyclePhase({ singular: true }); + const phase = new LifecyclePhase(new Rx.Subscription(), { singular: true }); const beforeNotifs: Array> = []; phase.before$.pipe(materialize()).subscribe((n) => beforeNotifs.push(n)); @@ -160,7 +160,7 @@ describe('without randomness', () => { }); it('completes before$ subscribers after trigger of singular phase', async () => { - const phase = new LifecyclePhase({ singular: true }); + const phase = new LifecyclePhase(new Rx.Subscription(), { singular: true }); await phase.trigger(); await expect(phase.before$.pipe(materialize(), toArray()).toPromise()).resolves @@ -177,7 +177,7 @@ describe('without randomness', () => { }); it('replays after$ event subscribers after trigger of singular phase', async () => { - const phase = new LifecyclePhase({ singular: true }); + const phase = new LifecyclePhase(new Rx.Subscription(), { singular: true }); await phase.trigger(); await expect(phase.after$.pipe(materialize(), toArray()).toPromise()).resolves diff --git a/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.ts b/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.ts index 09e7c6f3b8d15..df4b26230d4da 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/lifecycle_phase.ts @@ -26,6 +26,7 @@ export class LifecyclePhase { public readonly after$: Rx.Observable; constructor( + sub: Rx.Subscription, private readonly options: { singular?: boolean; } = {} @@ -35,6 +36,12 @@ export class LifecyclePhase { this.afterSubj = this.options.singular ? new Rx.ReplaySubject(1) : new Rx.Subject(); this.after$ = this.afterSubj.asObservable(); + + sub.add(() => { + this.beforeSubj.complete(); + this.afterSubj.complete(); + this.handlers.length = 0; + }); } public add(fn: (...args: Args) => Promise | void) { diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js index b9d4ed6ef7b5e..62104cebf9cba 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js @@ -12,6 +12,32 @@ import { createAssignmentProxy } from './assignment_proxy'; import { wrapFunction } from './wrap_function'; import { wrapRunnableArgs } from './wrap_runnable_args'; +const allTestsSkippedCache = new WeakMap(); +function allTestsAreSkipped(suite) { + // cache result for each suite so we don't have to traverse over and over + const cache = allTestsSkippedCache.get(suite); + if (cache) { + return cache; + } + + // if this suite is skipped directly then all it's children are skipped + if (suite.pending) { + allTestsSkippedCache.set(suite, true); + return true; + } + + // if any of this suites own tests are not skipped, then we don't need to traverse to child suites + if (suite.tests.some((t) => !t.pending)) { + allTestsSkippedCache.set(suite, false); + return false; + } + + // otherwise traverse down through the child suites and return true only if all children are all skipped + const childrenSkipped = suite.suites.every(allTestsAreSkipped); + allTestsSkippedCache.set(suite, childrenSkipped); + return childrenSkipped; +} + export function decorateMochaUi(log, lifecycle, context, { rootTags }) { // incremented at the start of each suite, decremented after // so that in each non-suite call we can know if we are within @@ -71,6 +97,12 @@ export function decorateMochaUi(log, lifecycle, context, { rootTags }) { provider.call(this); + if (allTestsAreSkipped(this)) { + // all the children in this suite are skipped, so make sure the suite is + // marked as pending so that its hooks are not run + this.pending = true; + } + after('afterTestSuite.trigger', async () => { await lifecycle.afterTestSuite.trigger(this); }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.test.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.test.js index 191503af123d0..3d1867aa0eed0 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.test.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.test.js @@ -69,6 +69,9 @@ function setup({ include, exclude, esVersion }) { info(...args) { history.push(`info: ${format(...args)}`); }, + debug(...args) { + history.push(`debg: ${format(...args)}`); + }, }, mocha, include, @@ -221,7 +224,7 @@ it(`excludes tests which don't meet the esVersionRequirement`, async () => { expect(history).toMatchInlineSnapshot(` Array [ - "info: Only running suites which are compatible with ES version 9.0.0", + "debg: Only running suites which are compatible with ES version 9.0.0", "suite: ", "suite: level 1", "suite: level 1 level 1a", diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.ts b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.ts index 98db434b3b088..6bb95acd407de 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.ts @@ -44,7 +44,7 @@ export function filterSuites({ log, mocha, include, exclude, esVersion }: Option if (esVersion) { // traverse the test graph and exclude any tests which don't meet their esVersionRequirement - log.info('Only running suites which are compatible with ES version', esVersion.toString()); + log.debug('Only running suites which are compatible with ES version', esVersion.toString()); (function recurse(parentSuite: SuiteInternal) { const children = parentSuite.suites; parentSuite.suites = []; diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts index ee993122d7d9c..96900555db745 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts @@ -17,7 +17,6 @@ import { import { Config } from '../../config'; import { Runner } from '../../../fake_mocha_types'; -import { TestMetadata, ScreenshotRecord } from '../../test_metadata'; import { Lifecycle } from '../../lifecycle'; import { getSnapshotOfRunnableLogs } from '../../../../mocha'; @@ -36,7 +35,6 @@ interface Runnable { file: string; title: string; parent: Suite; - _screenshots?: ScreenshotRecord[]; } function getHookType(hook: Runnable): CiStatsTestType { @@ -60,15 +58,18 @@ export function setupCiStatsFtrTestGroupReporter({ config, lifecycle, runner, - testMetadata, reporter, }: { config: Config; lifecycle: Lifecycle; runner: Runner; - testMetadata: TestMetadata; reporter: CiStatsReporter; }) { + const testGroupType = process.env.TEST_GROUP_TYPE_FUNCTIONAL; + if (!testGroupType) { + throw new Error('missing process.env.TEST_GROUP_TYPE_FUNCTIONAL'); + } + let startMs: number | undefined; runner.on('start', () => { startMs = Date.now(); @@ -78,7 +79,7 @@ export function setupCiStatsFtrTestGroupReporter({ const group: CiStatsReportTestsOptions['group'] = { startTime: new Date(start).toJSON(), durationMs: 0, - type: config.path.startsWith('x-pack') ? 'X-Pack Functional Tests' : 'Functional Tests', + type: testGroupType, name: Path.relative(REPO_ROOT, config.path), result: 'skip', meta: { @@ -106,10 +107,6 @@ export function setupCiStatsFtrTestGroupReporter({ type, error: error?.stack, stdout: getSnapshotOfRunnableLogs(runnable), - screenshots: testMetadata.getScreenshots(runnable).map((s) => ({ - base64Png: s.base64Png, - name: s.name, - })), }); } diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js index 973a552ebb728..66a4c9ce4fd04 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js @@ -24,7 +24,6 @@ export function MochaReporterProvider({ getService }) { const log = getService('log'); const config = getService('config'); const lifecycle = getService('lifecycle'); - const testMetadata = getService('testMetadata'); let originalLogWriters; let reporterCaptureStartTime; @@ -61,7 +60,6 @@ export function MochaReporterProvider({ getService }) { config, lifecycle, runner, - testMetadata, }); } } diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js index 4f798839d7231..a0298b635a135 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/validate_ci_group_tags.js @@ -48,8 +48,10 @@ export function validateCiGroupTags(log, mocha) { const queue = [mocha.suite]; while (queue.length) { const suite = queue.shift(); - if (getCiGroups(suite).length > 1) { - suitesWithMultipleCiGroups.push(suite); + if (getCiGroups(suite).length) { + throw new Error( + 'ciGroups are no longer needed and should be removed. If you need to split up your FTR config because it is taking too long to complete then create one or more a new FTR config files and split your test files amoungst them' + ); } else { queue.push(...(suite.suites ?? [])); } diff --git a/packages/kbn-test/src/functional_test_runner/lib/providers/index.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/index.ts index 578e41ca8e827..c0b85370d321c 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/providers/index.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/providers/index.ts @@ -7,6 +7,6 @@ */ export { ProviderCollection } from './provider_collection'; -export { readProviderSpec } from './read_provider_spec'; +export * from './read_provider_spec'; export { createAsyncInstance } from './async_instance'; export type { Provider } from './read_provider_spec'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts index c199dfc092789..403708b893db8 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/snapshots/decorate_snapshot_ui.test.ts @@ -6,11 +6,14 @@ * Side Public License, v 1. */ +import path from 'path'; +import fs from 'fs'; + +import { ToolingLog } from '@kbn/tooling-log'; + import { Suite, Test } from '../../fake_mocha_types'; import { Lifecycle } from '../lifecycle'; import { decorateSnapshotUi, expectSnapshot } from './decorate_snapshot_ui'; -import path from 'path'; -import fs from 'fs'; const createRootSuite = () => { const suite = { @@ -65,7 +68,7 @@ describe('decorateSnapshotUi', () => { let lifecycle: Lifecycle; let rootSuite: Suite; beforeEach(async () => { - lifecycle = new Lifecycle(); + lifecycle = new Lifecycle(new ToolingLog()); rootSuite = createRootSuite(); decorateSnapshotUi({ lifecycle, updateSnapshots: false, isCi: false }); @@ -116,7 +119,7 @@ describe('decorateSnapshotUi', () => { let lifecycle: Lifecycle; let rootSuite: Suite; beforeEach(async () => { - lifecycle = new Lifecycle(); + lifecycle = new Lifecycle(new ToolingLog()); rootSuite = createRootSuite(); decorateSnapshotUi({ lifecycle, updateSnapshots: false, isCi: false }); @@ -162,7 +165,7 @@ exports[\`Test2 1\`] = \`"bar"\`; let lifecycle: Lifecycle; let rootSuite: Suite; beforeEach(async () => { - lifecycle = new Lifecycle(); + lifecycle = new Lifecycle(new ToolingLog()); rootSuite = createRootSuite(); decorateSnapshotUi({ lifecycle, updateSnapshots: true, isCi: false }); @@ -185,7 +188,7 @@ exports[\`Test2 1\`] = \`"bar"\`; fs.writeFileSync( snapshotFile, `// Jest Snapshot v1, https://goo.gl/fbAQLP - + exports[\`Test 1\`] = \`"foo"\`; `, { encoding: 'utf-8' } @@ -219,7 +222,7 @@ exports[\`Test2 1\`] = \`"bar"\`; let lifecycle: Lifecycle; let rootSuite: Suite; beforeEach(async () => { - lifecycle = new Lifecycle(); + lifecycle = new Lifecycle(new ToolingLog()); rootSuite = createRootSuite(); decorateSnapshotUi({ lifecycle, updateSnapshots: false, isCi: true }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts b/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts index 53ce4c74c1388..43f1508ab7938 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts @@ -9,6 +9,8 @@ import fs from 'fs'; import { join, resolve } from 'path'; +import { ToolingLog } from '@kbn/tooling-log'; + jest.mock('fs'); jest.mock('@kbn/utils', () => { return { REPO_ROOT: '/dev/null/root' }; @@ -60,7 +62,7 @@ describe('SuiteTracker', () => { }; const runLifecycleWithMocks = async (mocks: Suite[], fn: (objs: any) => any = () => {}) => { - const lifecycle = new Lifecycle(); + const lifecycle = new Lifecycle(new ToolingLog()); const suiteTracker = SuiteTracker.startTracking( lifecycle, resolve(REPO_ROOT, MOCK_CONFIG_PATH) diff --git a/packages/kbn-test/src/functional_test_runner/lib/test_metadata.ts b/packages/kbn-test/src/functional_test_runner/lib/test_metadata.ts deleted file mode 100644 index 5789231f87044..0000000000000 --- a/packages/kbn-test/src/functional_test_runner/lib/test_metadata.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Lifecycle } from './lifecycle'; - -export interface ScreenshotRecord { - name: string; - base64Png: string; - baselinePath?: string; - failurePath?: string; -} - -export class TestMetadata { - // mocha's global types mean we can't import Mocha or it will override the global jest types.............. - private currentRunnable?: any; - - constructor(lifecycle: Lifecycle) { - lifecycle.beforeEachRunnable.add((runnable) => { - this.currentRunnable = runnable; - }); - } - - addScreenshot(screenshot: ScreenshotRecord) { - this.currentRunnable._screenshots = (this.currentRunnable._screenshots || []).concat( - screenshot - ); - } - - getScreenshots(test: any): ScreenshotRecord[] { - if (!test || typeof test !== 'object' || !test._screenshots) { - return []; - } - - return test._screenshots.slice(); - } -} diff --git a/packages/kbn-test/src/functional_test_runner/public_types.ts b/packages/kbn-test/src/functional_test_runner/public_types.ts index 2d632b28d6e21..67adceaf22323 100644 --- a/packages/kbn-test/src/functional_test_runner/public_types.ts +++ b/packages/kbn-test/src/functional_test_runner/public_types.ts @@ -8,10 +8,10 @@ import type { ToolingLog } from '@kbn/tooling-log'; -import type { Config, Lifecycle, TestMetadata, DockerServersService, EsVersion } from './lib'; +import type { Config, Lifecycle, DockerServersService, EsVersion } from './lib'; import type { Test, Suite } from './fake_mocha_types'; -export { Lifecycle, Config, TestMetadata }; +export { Lifecycle, Config }; export interface AsyncInstance { /** @@ -56,9 +56,7 @@ export interface GenericFtrProviderContext< * Determine if a service is avaliable * @param serviceName */ - hasService( - serviceName: 'config' | 'log' | 'lifecycle' | 'testMetadata' | 'dockerServers' | 'esVersion' - ): true; + hasService(serviceName: 'config' | 'log' | 'lifecycle' | 'dockerServers' | 'esVersion'): true; hasService(serviceName: K): serviceName is K; hasService(serviceName: string): serviceName is Extract; @@ -71,7 +69,6 @@ export interface GenericFtrProviderContext< getService(serviceName: 'log'): ToolingLog; getService(serviceName: 'lifecycle'): Lifecycle; getService(serviceName: 'dockerServers'): DockerServersService; - getService(serviceName: 'testMetadata'): TestMetadata; getService(serviceName: 'esVersion'): EsVersion; getService(serviceName: T): ServiceMap[T]; diff --git a/packages/kbn-test/src/functional_tests/lib/run_ftr.ts b/packages/kbn-test/src/functional_tests/lib/run_ftr.ts index 6887a664d6657..4c4a7128a05a9 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_ftr.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_ftr.ts @@ -90,6 +90,9 @@ export async function assertNoneExcluded({ configPath, options }: CreateFtrParam } const stats = await ftr.getTestStats(); + if (!stats) { + throw new Error('unable to get test stats'); + } if (stats.testsExcludedByTag.length > 0) { throw new CliError(` ${stats.testsExcludedByTag.length} tests in the ${configPath} config @@ -122,5 +125,8 @@ export async function hasTests({ configPath, options }: CreateFtrParams) { return true; } const stats = await ftr.getTestStats(); - return stats.testCount > 0; + if (!stats) { + throw new Error('unable to get test stats'); + } + return stats.nonSkippedTestCount > 0; } diff --git a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts index 6305e522b3929..47d0b1c93b620 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts @@ -36,13 +36,13 @@ export async function runKibanaServer({ config: Config; options: { installDir?: string; extraKbnOpts?: string[] }; }) { - const { installDir } = options; const runOptions = config.get('kbnTestServer.runOptions'); + const installDir = runOptions.alwaysUseSource ? undefined : options.installDir; const env = config.get('kbnTestServer.env'); await procs.run('kibana', { cmd: getKibanaCmd(installDir), - args: filterCliArgs(collectCliArgs(config, options)), + args: filterCliArgs(collectCliArgs(config, installDir, options.extraKbnOpts)), env: { FORCE_COLOR: 1, ...process.env, @@ -70,10 +70,7 @@ function getKibanaCmd(installDir?: string) { * passed, we run from source code. We also allow passing in extra * Kibana server options, so we tack those on here. */ -function collectCliArgs( - config: Config, - { installDir, extraKbnOpts }: { installDir?: string; extraKbnOpts?: string[] } -) { +function collectCliArgs(config: Config, installDir?: string, extraKbnOpts: string[] = []) { const buildArgs: string[] = config.get('kbnTestServer.buildArgs') || []; const sourceArgs: string[] = config.get('kbnTestServer.sourceArgs') || []; const serverArgs: string[] = config.get('kbnTestServer.serverArgs') || []; @@ -82,7 +79,7 @@ function collectCliArgs( serverArgs, (args) => (installDir ? args.filter((a: string) => a !== '--oss') : args), (args) => (installDir ? [...buildArgs, ...args] : [KIBANA_EXEC_PATH, ...sourceArgs, ...args]), - (args) => args.concat(extraKbnOpts || []) + (args) => args.concat(extraKbnOpts) ); } diff --git a/packages/kbn-test/src/functional_tests/tasks.ts b/packages/kbn-test/src/functional_tests/tasks.ts index 8116fa25650a1..dd9fe4c93016c 100644 --- a/packages/kbn-test/src/functional_tests/tasks.ts +++ b/packages/kbn-test/src/functional_tests/tasks.ts @@ -8,6 +8,7 @@ import { relative } from 'path'; import * as Rx from 'rxjs'; +import { setTimeout } from 'timers/promises'; import { startWith, switchMap, take } from 'rxjs/operators'; import { withProcRunner } from '@kbn/dev-utils'; import { ToolingLog } from '@kbn/tooling-log'; @@ -63,7 +64,7 @@ interface RunTestsParams extends CreateFtrOptions { assertNoneExcluded: boolean; } export async function runTests(options: RunTestsParams) { - if (!process.env.KBN_NP_PLUGINS_BUILT && !options.assertNoneExcluded) { + if (!process.env.CI && !options.assertNoneExcluded) { const log = options.createLogger(); log.warning('❗️❗️❗️'); log.warning('❗️❗️❗️'); @@ -91,21 +92,18 @@ export async function runTests(options: RunTestsParams) { return; } - log.write('--- determining which ftr configs to run'); - const configPathsWithTests: string[] = []; - for (const configPath of options.configs) { - log.info('testing', relative(REPO_ROOT, configPath)); - await log.indent(4, async () => { - if (await hasTests({ configPath, options: { ...options, log } })) { - configPathsWithTests.push(configPath); + for (const [i, configPath] of options.configs.entries()) { + await log.indent(0, async () => { + if (options.configs.length > 1) { + const progress = `${i + 1}/${options.configs.length}`; + log.write(`--- [${progress}] Running ${relative(REPO_ROOT, configPath)}`); } - }); - } - for (const [i, configPath] of configPathsWithTests.entries()) { - await log.indent(0, async () => { - const progress = `${i + 1}/${configPathsWithTests.length}`; - log.write(`--- [${progress}] Running ${relative(REPO_ROOT, configPath)}`); + if (!(await hasTests({ configPath, options: { ...options, log } }))) { + // just run the FTR, no Kibana or ES, which will quickly report a skipped test group to ci-stats and continue + await runFtr({ configPath, options: { ...options, log } }); + return; + } await withProcRunner(log, async (procs) => { const config = await readConfigFile(log, options.esVersion, configPath); @@ -122,7 +120,7 @@ export async function runTests(options: RunTestsParams) { const delay = config.get('kbnTestServer.delayShutdown'); if (typeof delay === 'number') { log.info('Delaying shutdown of Kibana for', delay, 'ms'); - await new Promise((r) => setTimeout(r, delay)); + await setTimeout(delay); } await procs.stop('kibana'); diff --git a/scripts/README.md b/scripts/README.md index 960e8ab2ed0b8..a743ce5e1d53d 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -17,13 +17,13 @@ This directory is excluded from the build and tools within it should help users ## Functional Test Scripts -**`node scripts/functional_tests [--config test/functional/config.js --config test/api_integration/config.js]`** +**`node scripts/functional_tests [--config test/functional/config.base.js --config test/api_integration/config.js]`** Runs all the functional tests: selenium tests and api integration tests. List configs with multiple `--config` arguments. Uses the [@kbn/test](../packages/kbn-test) library to run Elasticsearch and Kibana servers and tests against those servers, for multiple server+test setups. In particular, calls out to [`runTests()`](../packages/kbn-test/src/functional_tests/tasks.js). Can be run on a single config. -**`node scripts/functional_tests_server [--config test/functional/config.js]`** +**`node scripts/functional_tests_server [--config test/functional/config.base.js]`** -Starts just the Elasticsearch and Kibana servers given a single config, i.e. via `--config test/functional/config.js` or `--config test/api_integration/config`. Allows the user to start just the servers with this script, and keep them running while running tests against these servers. The idea is that the same config file configures both Elasticsearch and Kibana servers. Uses the [`startServers()`](../packages/kbn-test/src/functional_tests/tasks.js#L52-L80) method from [@kbn/test](../packages/kbn-test) library. +Starts just the Elasticsearch and Kibana servers given a single config, i.e. via `--config test/functional/config.base.js` or `--config test/api_integration/config`. Allows the user to start just the servers with this script, and keep them running while running tests against these servers. The idea is that the same config file configures both Elasticsearch and Kibana servers. Uses the [`startServers()`](../packages/kbn-test/src/functional_tests/tasks.js#L52-L80) method from [@kbn/test](../packages/kbn-test) library. Example. Start servers _and_ run tests, separately, but using the same config: @@ -51,7 +51,7 @@ If you wish to load up specific es archived data for your test, you can do so vi node scripts/es_archiver.js load [--es-url=http://username:password@localhost:9200] [--kibana-url=http://username:password@localhost:5601/{basepath?}] ``` -That will load the specified archive located in the archive directory specified by the default functional config file, located in `test/functional/config.js`. To load archives from other function config files you can pass `--config path/to/config.js`. +That will load the specified archive located in the archive directory specified by the default functional config file, located in `test/functional/config.base.js`. To load archives from other function config files you can pass `--config path/to/config.js`. *Note:* The `--es-url` and `--kibana-url` options may or may not be neccessary depending on your current Kibana configuration settings, and their values may also change based on those settings (for example if you are not running with security you will not need the `username:password` portion). diff --git a/scripts/extract_performance_testing_dataset.js b/scripts/extract_performance_testing_dataset.js new file mode 100644 index 0000000000000..deb3da481f1e1 --- /dev/null +++ b/scripts/extract_performance_testing_dataset.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../src/setup_node_env'); +require('@kbn/performance-testing-dataset-extractor').runExtractor(); diff --git a/scripts/find_target_node_imports_in_bundles.js b/scripts/find_target_node_imports_in_bundles.js new file mode 100644 index 0000000000000..eae3b94efeaba --- /dev/null +++ b/scripts/find_target_node_imports_in_bundles.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../src/setup_node_env/no_transpilation'); +require('@kbn/optimizer').runFindTargetNodeImportsCli(); diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 1e963660a1e03..eb1dea2dcab36 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -7,26 +7,4 @@ */ require('../src/setup_node_env'); -require('@kbn/test').runTestsCli([ - require.resolve('../test/functional/config.ccs.ts'), - require.resolve('../test/functional/config.js'), - require.resolve('../test/plugin_functional/config.ts'), - require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), - require.resolve('../test/new_visualize_flow/config.ts'), - require.resolve('../test/interactive_setup_api_integration/enrollment_flow.config.ts'), - require.resolve('../test/interactive_setup_api_integration/manual_configuration_flow.config.ts'), - require.resolve( - '../test/interactive_setup_api_integration/manual_configuration_flow_without_tls.config.ts' - ), - require.resolve('../test/interactive_setup_functional/enrollment_token.config.ts'), - require.resolve('../test/interactive_setup_functional/manual_configuration.config.ts'), - require.resolve( - '../test/interactive_setup_functional/manual_configuration_without_security.config.ts' - ), - require.resolve( - '../test/interactive_setup_functional/manual_configuration_without_tls.config.ts' - ), - require.resolve('../test/api_integration/config.js'), - require.resolve('../test/interpreter_functional/config.ts'), - require.resolve('../test/examples/config.js'), -]); +require('@kbn/test').runTestsCli(); diff --git a/scripts/functional_tests_server.js b/scripts/functional_tests_server.js index 4995eba4de670..836a1ede126e3 100644 --- a/scripts/functional_tests_server.js +++ b/scripts/functional_tests_server.js @@ -7,4 +7,4 @@ */ require('../src/setup_node_env'); -require('@kbn/test').startServersCli(require.resolve('../test/functional/config.js')); +require('@kbn/test').startServersCli(require.resolve('../test/functional/config.base.js')); diff --git a/src/core/public/analytics/analytics_service.mock.ts b/src/core/public/analytics/analytics_service.mock.ts index c14d4a8216850..9037a756473c3 100644 --- a/src/core/public/analytics/analytics_service.mock.ts +++ b/src/core/public/analytics/analytics_service.mock.ts @@ -40,6 +40,7 @@ const createAnalyticsServiceMock = (): jest.Mocked => return { setup: jest.fn().mockImplementation(createAnalyticsServiceSetup), start: jest.fn().mockImplementation(createAnalyticsServiceStart), + stop: jest.fn(), }; }; diff --git a/src/core/public/analytics/analytics_service.test.mocks.ts b/src/core/public/analytics/analytics_service.test.mocks.ts new file mode 100644 index 0000000000000..3d98cf4392926 --- /dev/null +++ b/src/core/public/analytics/analytics_service.test.mocks.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 { AnalyticsClient } from '@kbn/analytics-client'; +import { Subject } from 'rxjs'; + +export const analyticsClientMock: jest.Mocked = { + optIn: jest.fn(), + reportEvent: jest.fn(), + registerEventType: jest.fn(), + registerContextProvider: jest.fn(), + removeContextProvider: jest.fn(), + registerShipper: jest.fn(), + telemetryCounter$: new Subject(), + shutdown: jest.fn(), +}; + +jest.doMock('@kbn/analytics-client', () => ({ + createAnalytics: () => analyticsClientMock, +})); diff --git a/src/core/public/analytics/analytics_service.test.ts b/src/core/public/analytics/analytics_service.test.ts new file mode 100644 index 0000000000000..e2298a79ff134 --- /dev/null +++ b/src/core/public/analytics/analytics_service.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { firstValueFrom, Observable } from 'rxjs'; +import { analyticsClientMock } from './analytics_service.test.mocks'; +import { coreMock, injectedMetadataServiceMock } from '../mocks'; +import { AnalyticsService } from './analytics_service'; + +describe('AnalyticsService', () => { + let analyticsService: AnalyticsService; + beforeEach(() => { + jest.clearAllMocks(); + analyticsService = new AnalyticsService(coreMock.createCoreContext()); + }); + test('should register some context providers on creation', async () => { + expect(analyticsClientMock.registerContextProvider).toHaveBeenCalledTimes(3); + await expect( + firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[0][0].context$) + ).resolves.toMatchInlineSnapshot(` + Object { + "branch": "branch", + "buildNum": 100, + "buildSha": "buildSha", + "isDev": true, + "isDistributable": false, + "version": "version", + } + `); + await expect( + firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[1][0].context$) + ).resolves.toEqual({ session_id: expect.any(String) }); + await expect( + firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[2][0].context$) + ).resolves.toEqual({ + preferred_language: 'en-US', + preferred_languages: ['en-US', 'en'], + user_agent: expect.any(String), + }); + }); + + test('setup should expose all the register APIs, reportEvent and opt-in', () => { + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + expect(analyticsService.setup({ injectedMetadata })).toStrictEqual({ + registerShipper: expect.any(Function), + registerContextProvider: expect.any(Function), + removeContextProvider: expect.any(Function), + registerEventType: expect.any(Function), + reportEvent: expect.any(Function), + optIn: expect.any(Function), + telemetryCounter$: expect.any(Observable), + }); + }); + + test('setup should register the elasticsearch info context provider (undefined)', async () => { + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + analyticsService.setup({ injectedMetadata }); + await expect( + firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[3][0].context$) + ).resolves.toMatchInlineSnapshot(`undefined`); + }); + + test('setup should register the elasticsearch info context provider (with info)', async () => { + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + injectedMetadata.getElasticsearchInfo.mockReturnValue({ + cluster_name: 'cluster_name', + cluster_uuid: 'cluster_uuid', + cluster_version: 'version', + }); + analyticsService.setup({ injectedMetadata }); + await expect( + firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[3][0].context$) + ).resolves.toMatchInlineSnapshot(` + Object { + "cluster_name": "cluster_name", + "cluster_uuid": "cluster_uuid", + "cluster_version": "version", + } + `); + }); + + test('setup should expose only the APIs report and opt-in', () => { + expect(analyticsService.start()).toStrictEqual({ + reportEvent: expect.any(Function), + optIn: expect.any(Function), + telemetryCounter$: expect.any(Observable), + }); + }); +}); diff --git a/src/core/public/analytics/analytics_service.ts b/src/core/public/analytics/analytics_service.ts index 89e83c7e3c85b..723122ffbaef2 100644 --- a/src/core/public/analytics/analytics_service.ts +++ b/src/core/public/analytics/analytics_service.ts @@ -8,7 +8,10 @@ import type { AnalyticsClient } from '@kbn/analytics-client'; import { createAnalytics } from '@kbn/analytics-client'; +import { of } from 'rxjs'; +import { InjectedMetadataSetup } from '../injected_metadata'; import { CoreContext } from '../core_system'; +import { getSessionId } from './get_session_id'; import { createLogger } from './logger'; /** @@ -16,7 +19,7 @@ import { createLogger } from './logger'; * {@link AnalyticsClient} * @public */ -export type AnalyticsServiceSetup = AnalyticsClient; +export type AnalyticsServiceSetup = Omit; /** * Exposes the public APIs of the AnalyticsClient during the start phase * {@link AnalyticsClient} @@ -27,6 +30,11 @@ export type AnalyticsServiceStart = Pick< 'optIn' | 'reportEvent' | 'telemetryCounter$' >; +/** @internal */ +export interface AnalyticsServiceSetupDeps { + injectedMetadata: InjectedMetadataSetup; +} + export class AnalyticsService { private readonly analyticsClient: AnalyticsClient; @@ -38,9 +46,18 @@ export class AnalyticsService { // For now, we are relying on whether it's a distributable or running from source. sendTo: core.env.packageInfo.dist ? 'production' : 'staging', }); + + this.registerBuildInfoAnalyticsContext(core); + + // We may eventually move the following to the client's package since they are not Kibana-specific + // and can benefit other consumers of the client. + this.registerSessionIdContext(); + this.registerBrowserInfoAnalyticsContext(); } - public setup(): AnalyticsServiceSetup { + public setup({ injectedMetadata }: AnalyticsServiceSetupDeps): AnalyticsServiceSetup { + this.registerElasticsearchInfoContext(injectedMetadata); + return { optIn: this.analyticsClient.optIn, registerContextProvider: this.analyticsClient.registerContextProvider, @@ -51,6 +68,7 @@ export class AnalyticsService { telemetryCounter$: this.analyticsClient.telemetryCounter$, }; } + public start(): AnalyticsServiceStart { return { optIn: this.analyticsClient.optIn, @@ -58,4 +76,119 @@ export class AnalyticsService { telemetryCounter$: this.analyticsClient.telemetryCounter$, }; } + + public stop() { + this.analyticsClient.shutdown(); + } + + /** + * Enriches the events with a session_id, so we can correlate them and understand funnels. + * @private + */ + private registerSessionIdContext() { + this.analyticsClient.registerContextProvider({ + name: 'session-id', + context$: of({ session_id: getSessionId() }), + schema: { + session_id: { + type: 'keyword', + _meta: { description: 'Unique session ID for every browser session' }, + }, + }, + }); + } + + /** + * Enriches the event with the build information. + * @param core The core context. + * @private + */ + private registerBuildInfoAnalyticsContext(core: CoreContext) { + this.analyticsClient.registerContextProvider({ + name: 'build info', + context$: of({ + isDev: core.env.mode.dev, + isDistributable: core.env.packageInfo.dist, + version: core.env.packageInfo.version, + branch: core.env.packageInfo.branch, + buildNum: core.env.packageInfo.buildNum, + buildSha: core.env.packageInfo.buildSha, + }), + schema: { + isDev: { + type: 'boolean', + _meta: { description: 'Is it running in development mode?' }, + }, + isDistributable: { + type: 'boolean', + _meta: { description: 'Is it running from a distributable?' }, + }, + version: { type: 'keyword', _meta: { description: 'Version of the Kibana instance.' } }, + branch: { + type: 'keyword', + _meta: { description: 'Branch of source running Kibana from.' }, + }, + buildNum: { type: 'long', _meta: { description: 'Build number of the Kibana instance.' } }, + buildSha: { type: 'keyword', _meta: { description: 'Build SHA of the Kibana instance.' } }, + }, + }); + } + + /** + * Enriches events with the current Browser's information + * @private + */ + private registerBrowserInfoAnalyticsContext() { + this.analyticsClient.registerContextProvider({ + name: 'browser info', + context$: of({ + user_agent: navigator.userAgent, + preferred_language: navigator.language, + preferred_languages: navigator.languages, + }), + schema: { + user_agent: { + type: 'keyword', + _meta: { description: 'User agent of the browser.' }, + }, + preferred_language: { + type: 'keyword', + _meta: { description: 'Preferred language of the browser.' }, + }, + preferred_languages: { + type: 'array', + items: { + type: 'keyword', + _meta: { description: 'List of the preferred languages of the browser.' }, + }, + }, + }, + }); + } + + /** + * Enriches the events with the Elasticsearch info (cluster name, uuid and version). + * @param injectedMetadata The injected metadata service. + * @private + */ + private registerElasticsearchInfoContext(injectedMetadata: InjectedMetadataSetup) { + this.analyticsClient.registerContextProvider({ + name: 'elasticsearch info', + context$: of(injectedMetadata.getElasticsearchInfo()), + schema: { + cluster_name: { + type: 'keyword', + _meta: { description: 'The Cluster Name', optional: true }, + }, + cluster_uuid: { + type: 'keyword', + _meta: { description: 'The Cluster UUID', optional: true }, + }, + cluster_version: { + type: 'keyword', + _meta: { description: 'The Cluster version', optional: true }, + }, + }, + }); + } } diff --git a/src/core/public/analytics/get_session_id.test.ts b/src/core/public/analytics/get_session_id.test.ts new file mode 100644 index 0000000000000..85ac515e29f68 --- /dev/null +++ b/src/core/public/analytics/get_session_id.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getSessionId } from './get_session_id'; + +describe('getSessionId', () => { + test('should return a session id', () => { + const sessionId = getSessionId(); + expect(sessionId).toStrictEqual(expect.any(String)); + }); + + test('calling it twice should return the same value', () => { + const sessionId1 = getSessionId(); + const sessionId2 = getSessionId(); + expect(sessionId2).toStrictEqual(sessionId1); + }); +}); diff --git a/src/core/public/analytics/get_session_id.ts b/src/core/public/analytics/get_session_id.ts new file mode 100644 index 0000000000000..62bb3a4a1c336 --- /dev/null +++ b/src/core/public/analytics/get_session_id.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { v4 } from 'uuid'; + +/** + * Returns a session ID for the current user. + * We are storing it to the sessionStorage. This means it remains the same through refreshes, + * but it is not persisted when closing the browser/tab or manually navigating to another URL. + */ +export function getSessionId(): string { + const sessionId = sessionStorage.getItem('sessionId') ?? v4(); + sessionStorage.setItem('sessionId', sessionId); + return sessionId; +} diff --git a/src/core/public/analytics/logger.test.ts b/src/core/public/analytics/logger.test.ts new file mode 100644 index 0000000000000..2fbe17e3f7d22 --- /dev/null +++ b/src/core/public/analytics/logger.test.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { LogRecord } from '@kbn/logging'; +import { createLogger } from './logger'; + +describe('createLogger', () => { + // Calling `.mockImplementation` on all of them to avoid jest logging the console usage + const logErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + const logWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const logInfoSpy = jest.spyOn(console, 'info').mockImplementation(); + const logDebugSpy = jest.spyOn(console, 'debug').mockImplementation(); + const logTraceSpy = jest.spyOn(console, 'trace').mockImplementation(); + const logLogSpy = jest.spyOn(console, 'log').mockImplementation(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should create a logger', () => { + const logger = createLogger(false); + expect(logger).toStrictEqual( + expect.objectContaining({ + fatal: expect.any(Function), + error: expect.any(Function), + warn: expect.any(Function), + info: expect.any(Function), + debug: expect.any(Function), + trace: expect.any(Function), + log: expect.any(Function), + get: expect.any(Function), + }) + ); + }); + + test('when isDev === false, it should not log anything', () => { + const logger = createLogger(false); + logger.fatal('fatal'); + expect(logErrorSpy).not.toHaveBeenCalled(); + logger.error('error'); + expect(logErrorSpy).not.toHaveBeenCalled(); + logger.warn('warn'); + expect(logWarnSpy).not.toHaveBeenCalled(); + logger.info('info'); + expect(logInfoSpy).not.toHaveBeenCalled(); + logger.debug('debug'); + expect(logDebugSpy).not.toHaveBeenCalled(); + logger.trace('trace'); + expect(logTraceSpy).not.toHaveBeenCalled(); + logger.log({} as LogRecord); + expect(logLogSpy).not.toHaveBeenCalled(); + logger.get().warn('warn'); + expect(logWarnSpy).not.toHaveBeenCalled(); + }); + + test('when isDev === true, it should log everything', () => { + const logger = createLogger(true); + logger.fatal('fatal'); + expect(logErrorSpy).toHaveBeenCalledTimes(1); + logger.error('error'); + expect(logErrorSpy).toHaveBeenCalledTimes(2); // fatal + error + logger.warn('warn'); + expect(logWarnSpy).toHaveBeenCalledTimes(1); + logger.info('info'); + expect(logInfoSpy).toHaveBeenCalledTimes(1); + logger.debug('debug'); + expect(logDebugSpy).toHaveBeenCalledTimes(1); + logger.trace('trace'); + expect(logTraceSpy).toHaveBeenCalledTimes(1); + logger.log({} as LogRecord); + expect(logLogSpy).toHaveBeenCalledTimes(1); + logger.get().warn('warn'); + expect(logWarnSpy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index 6eddf08cd2ae1..ff24cc8839794 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -21,6 +21,20 @@ import { renderingServiceMock } from './rendering/rendering_service.mock'; import { integrationsServiceMock } from './integrations/integrations_service.mock'; import { coreAppMock } from './core_app/core_app.mock'; import { themeServiceMock } from './theme/theme_service.mock'; +import { analyticsServiceMock } from './analytics/analytics_service.mock'; + +export const analyticsServiceStartMock = analyticsServiceMock.createAnalyticsServiceStart(); +export const MockAnalyticsService = analyticsServiceMock.create(); +MockAnalyticsService.start.mockReturnValue(analyticsServiceStartMock); +export const AnalyticsServiceConstructor = jest.fn().mockReturnValue(MockAnalyticsService); +jest.doMock('./analytics', () => ({ + AnalyticsService: AnalyticsServiceConstructor, +})); + +export const fetchOptionalMemoryInfoMock = jest.fn(); +jest.doMock('./fetch_optional_memory_info', () => ({ + fetchOptionalMemoryInfo: fetchOptionalMemoryInfoMock, +})); export const MockInjectedMetadataService = injectedMetadataServiceMock.create(); export const InjectedMetadataServiceConstructor = jest diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 553c1668951e8..2a57364c9f93f 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -34,6 +34,10 @@ import { MockCoreApp, MockThemeService, ThemeServiceConstructor, + AnalyticsServiceConstructor, + MockAnalyticsService, + analyticsServiceStartMock, + fetchOptionalMemoryInfoMock, } from './core_system.test.mocks'; import { CoreSystem } from './core_system'; @@ -56,6 +60,7 @@ const defaultCoreSystemParams = { }, packageInfo: { dist: false, + version: '1.2.3', }, }, version: 'version', @@ -90,6 +95,7 @@ describe('constructor', () => { expect(IntegrationsServiceConstructor).toHaveBeenCalledTimes(1); expect(CoreAppConstructor).toHaveBeenCalledTimes(1); expect(ThemeServiceConstructor).toHaveBeenCalledTimes(1); + expect(AnalyticsServiceConstructor).toHaveBeenCalledTimes(1); }); it('passes injectedMetadata param to InjectedMetadataService', () => { @@ -146,6 +152,11 @@ describe('#setup()', () => { return core.setup(); } + it('calls analytics#setup()', async () => { + await setupCore(); + expect(MockAnalyticsService.setup).toHaveBeenCalledTimes(1); + }); + it('calls application#setup()', async () => { await setupCore(); expect(MockApplicationService.setup).toHaveBeenCalledTimes(1); @@ -222,6 +233,36 @@ describe('#start()', () => { ); }); + it('reports the event Loaded Kibana', async () => { + await startCore(); + expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledTimes(1); + expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledWith('Loaded Kibana', { + kibana_version: '1.2.3', + }); + }); + + it('reports the event Loaded Kibana (with memory)', async () => { + fetchOptionalMemoryInfoMock.mockReturnValue({ + memory_js_heap_size_limit: 3, + memory_js_heap_size_total: 2, + memory_js_heap_size_used: 1, + }); + + await startCore(); + expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledTimes(1); + expect(analyticsServiceStartMock.reportEvent).toHaveBeenCalledWith('Loaded Kibana', { + kibana_version: '1.2.3', + memory_js_heap_size_limit: 3, + memory_js_heap_size_total: 2, + memory_js_heap_size_used: 1, + }); + }); + + it('calls analytics#start()', async () => { + await startCore(); + expect(MockAnalyticsService.start).toHaveBeenCalledTimes(1); + }); + it('calls application#start()', async () => { await startCore(); expect(MockApplicationService.start).toHaveBeenCalledTimes(1); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 726f53a345208..9ea1f16f7f226 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -32,7 +32,9 @@ import { ThemeService } from './theme'; import { CoreApp } from './core_app'; import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; import { ExecutionContextService } from './execution_context'; +import type { AnalyticsServiceSetup } from './analytics'; import { AnalyticsService } from './analytics'; +import { fetchOptionalMemoryInfo } from './fetch_optional_memory_info'; interface Params { rootDomElement: HTMLElement; @@ -148,9 +150,10 @@ export class CoreSystem { await this.integrations.setup(); this.docLinks.setup(); - const analytics = this.analytics.setup(); + const analytics = this.analytics.setup({ injectedMetadata }); + this.registerLoadedKibanaEventType(analytics); - const executionContext = this.executionContext.setup(); + const executionContext = this.executionContext.setup({ analytics }); const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup, @@ -273,6 +276,11 @@ export class CoreSystem { targetDomElement: coreUiTargetDomElement, }); + analytics.reportEvent('Loaded Kibana', { + kibana_version: this.coreContext.env.packageInfo.version, + ...fetchOptionalMemoryInfo(), + }); + return { application, executionContext, @@ -300,6 +308,31 @@ export class CoreSystem { this.application.stop(); this.deprecations.stop(); this.theme.stop(); + this.analytics.stop(); this.rootDomElement.textContent = ''; } + + private registerLoadedKibanaEventType(analytics: AnalyticsServiceSetup) { + analytics.registerEventType({ + eventType: 'Loaded Kibana', + schema: { + kibana_version: { + type: 'keyword', + _meta: { description: 'The version of Kibana' }, + }, + memory_js_heap_size_limit: { + type: 'long', + _meta: { description: 'The maximum size of the heap', optional: true }, + }, + memory_js_heap_size_total: { + type: 'long', + _meta: { description: 'The total size of the heap', optional: true }, + }, + memory_js_heap_size_used: { + type: 'long', + _meta: { description: 'The used size of the heap', optional: true }, + }, + }, + }); + } } diff --git a/src/core/public/execution_context/execution_context_service.test.ts b/src/core/public/execution_context/execution_context_service.test.ts index 70e57b8993bb1..5c8f8bfae89f8 100644 --- a/src/core/public/execution_context/execution_context_service.test.ts +++ b/src/core/public/execution_context/execution_context_service.test.ts @@ -5,23 +5,45 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { ExecutionContextService, ExecutionContextSetup } from './execution_context_service'; +import type { AnalyticsServiceSetup } from '../analytics'; +import { analyticsServiceMock } from '../analytics/analytics_service.mock'; describe('ExecutionContextService', () => { let execContext: ExecutionContextSetup; let curApp$: BehaviorSubject; let execService: ExecutionContextService; + let analytics: jest.Mocked; beforeEach(() => { + analytics = analyticsServiceMock.createAnalyticsServiceSetup(); execService = new ExecutionContextService(); - execContext = execService.setup(); + execContext = execService.setup({ analytics }); curApp$ = new BehaviorSubject('app1'); execContext = execService.start({ curApp$, }); }); + it('should extend the analytics context', async () => { + expect(analytics.registerContextProvider).toHaveBeenCalledTimes(1); + const context$ = analytics.registerContextProvider.mock.calls[0][0].context$; + execContext.set({ + type: 'ghf', + description: 'first set', + }); + + await expect(firstValueFrom(context$)).resolves.toMatchInlineSnapshot(` + Object { + "applicationId": "app1", + "entityId": undefined, + "page": undefined, + "pageName": "ghf:app1", + } + `); + }); + it('app name updates automatically and clears everything else', () => { execContext.set({ type: 'ghf', diff --git a/src/core/public/execution_context/execution_context_service.ts b/src/core/public/execution_context/execution_context_service.ts index a14d876c9643c..c8d198b9c84f8 100644 --- a/src/core/public/execution_context/execution_context_service.ts +++ b/src/core/public/execution_context/execution_context_service.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { isEqual, isUndefined, omitBy } from 'lodash'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { compact, isEqual, isUndefined, omitBy } from 'lodash'; +import { BehaviorSubject, Observable, Subscription, map } from 'rxjs'; +import { AnalyticsServiceSetup } from '../analytics'; import { CoreService, KibanaExecutionContext } from '../../types'; // Should be exported from elastic/apm-rum @@ -55,6 +56,10 @@ export interface ExecutionContextSetup { */ export type ExecutionContextStart = ExecutionContextSetup; +export interface SetupDeps { + analytics: AnalyticsServiceSetup; +} + export interface StartDeps { curApp$: Observable; } @@ -68,7 +73,9 @@ export class ExecutionContextService private subscription: Subscription = new Subscription(); private contract?: ExecutionContextSetup; - public setup() { + public setup({ analytics }: SetupDeps) { + this.enrichAnalyticsContext(analytics); + this.contract = { context$: this.context$.asObservable(), clear: () => { @@ -134,4 +141,45 @@ export class ExecutionContextService ...context, }; } + + /** + * Sets the analytics context provider based on the execution context details. + * @param analytics The analytics service + * @private + */ + private enrichAnalyticsContext(analytics: AnalyticsServiceSetup) { + analytics.registerContextProvider({ + name: 'execution_context', + context$: this.context$.pipe( + map(({ type, name, page, id }) => ({ + pageName: `${compact([type, name, page]).join(':')}`, + applicationId: name ?? type ?? 'unknown', + page, + entityId: id, + })) + ), + schema: { + pageName: { + type: 'keyword', + _meta: { description: 'The name of the current page' }, + }, + page: { + type: 'keyword', + _meta: { description: 'The current page', optional: true }, + }, + applicationId: { + type: 'keyword', + _meta: { description: 'The id of the current application' }, + }, + entityId: { + type: 'keyword', + _meta: { + description: + 'The id of the current entity (dashboard, visualization, canvas, lens, etc)', + optional: true, + }, + }, + }, + }); + } } diff --git a/src/core/public/fetch_optional_memory_info.test.ts b/src/core/public/fetch_optional_memory_info.test.ts new file mode 100644 index 0000000000000..f92fad9c14d63 --- /dev/null +++ b/src/core/public/fetch_optional_memory_info.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { fetchOptionalMemoryInfo } from './fetch_optional_memory_info'; + +describe('fetchOptionalMemoryInfo', () => { + test('should return undefined if no memory info is available', () => { + expect(fetchOptionalMemoryInfo()).toBeUndefined(); + }); + + test('should return the memory info when available', () => { + // @ts-expect-error 2339 + window.performance.memory = { + get jsHeapSizeLimit() { + return 3; + }, + get totalJSHeapSize() { + return 2; + }, + get usedJSHeapSize() { + return 1; + }, + }; + expect(fetchOptionalMemoryInfo()).toEqual({ + memory_js_heap_size_limit: 3, + memory_js_heap_size_total: 2, + memory_js_heap_size_used: 1, + }); + }); +}); diff --git a/src/core/public/fetch_optional_memory_info.ts b/src/core/public/fetch_optional_memory_info.ts new file mode 100644 index 0000000000000..b18f3ca2698da --- /dev/null +++ b/src/core/public/fetch_optional_memory_info.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * `Performance.memory` output. + * https://developer.mozilla.org/en-US/docs/Web/API/Performance/memory + */ +export interface BrowserPerformanceMemoryInfo { + /** + * The maximum size of the heap, in bytes, that is available to the context. + */ + memory_js_heap_size_limit: number; + /** + * The total allocated heap size, in bytes. + */ + memory_js_heap_size_total: number; + /** + * The currently active segment of JS heap, in bytes. + */ + memory_js_heap_size_used: number; +} + +/** + * Get performance information from the browser (non-standard property). + * @remarks Only available in Google Chrome and MS Edge for now. + */ +export function fetchOptionalMemoryInfo(): BrowserPerformanceMemoryInfo | undefined { + // @ts-expect-error 2339 + const memory = window.performance.memory; + if (memory) { + return { + memory_js_heap_size_limit: memory.jsHeapSizeLimit, + memory_js_heap_size_total: memory.totalJSHeapSize, + memory_js_heap_size_used: memory.usedJSHeapSize, + }; + } +} diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index dc8fe63724411..83903942df53d 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -16,6 +16,7 @@ const createSetupContractMock = () => { getPublicBaseUrl: jest.fn(), getKibanaVersion: jest.fn(), getKibanaBranch: jest.fn(), + getElasticsearchInfo: jest.fn(), getCspConfig: jest.fn(), getExternalUrlConfig: jest.fn(), getAnonymousStatusPage: jest.fn(), diff --git a/src/core/public/injected_metadata/injected_metadata_service.test.ts b/src/core/public/injected_metadata/injected_metadata_service.test.ts index 3237401b38fa8..ba0e2470d7f26 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.test.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.test.ts @@ -9,6 +9,36 @@ import { DiscoveredPlugin } from '../../server'; import { InjectedMetadataService } from './injected_metadata_service'; +describe('setup.getElasticsearchInfo()', () => { + it('returns elasticsearch info from injectedMetadata', () => { + const setup = new InjectedMetadataService({ + injectedMetadata: { + clusterInfo: { + cluster_uuid: 'foo', + cluster_name: 'cluster_name', + cluster_version: 'version', + }, + }, + } as any).setup(); + + expect(setup.getElasticsearchInfo()).toEqual({ + cluster_uuid: 'foo', + cluster_name: 'cluster_name', + cluster_version: 'version', + }); + }); + + it('returns elasticsearch info as undefined if not present in the injectedMetadata', () => { + const setup = new InjectedMetadataService({ + injectedMetadata: { + clusterInfo: {}, + }, + } as any).setup(); + + expect(setup.getElasticsearchInfo()).toEqual({}); + }); +}); + describe('setup.getKibanaBuildNumber()', () => { it('returns buildNumber from injectedMetadata', () => { const setup = new InjectedMetadataService({ diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 07f56b889fc79..2e19da5c2cffe 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -27,6 +27,12 @@ export interface InjectedPluginMetadata { }; } +export interface InjectedMetadataClusterInfo { + cluster_uuid?: string; + cluster_name?: string; + cluster_version?: string; +} + /** @internal */ export interface InjectedMetadataParams { injectedMetadata: { @@ -36,6 +42,7 @@ export interface InjectedMetadataParams { basePath: string; serverBasePath: string; publicBaseUrl: string; + clusterInfo: InjectedMetadataClusterInfo; category?: AppCategory; csp: { warnLegacyBrowsers: boolean; @@ -143,6 +150,10 @@ export class InjectedMetadataService { getTheme: () => { return this.state.theme; }, + + getElasticsearchInfo: () => { + return this.state.clusterInfo; + }, }; } } @@ -169,6 +180,7 @@ export interface InjectedMetadataSetup { darkMode: boolean; version: ThemeVersion; }; + getElasticsearchInfo: () => InjectedMetadataClusterInfo; /** * An array of frontend plugins in topological order. */ diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 2a662e3f7f8bd..732ba71fcd2af 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -69,7 +69,7 @@ export { AnalyticsClient } // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver // // @public -export type AnalyticsServiceSetup = AnalyticsClient; +export type AnalyticsServiceSetup = Omit; // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver // @@ -1590,6 +1590,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:192:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:195:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/analytics/analytics_service.mock.ts b/src/core/server/analytics/analytics_service.mock.ts index 74f675ac37b8f..7a00e573f3e7b 100644 --- a/src/core/server/analytics/analytics_service.mock.ts +++ b/src/core/server/analytics/analytics_service.mock.ts @@ -54,6 +54,7 @@ const createAnalyticsServiceMock = (): jest.Mocked => preboot: jest.fn().mockImplementation(createAnalyticsServicePreboot), setup: jest.fn().mockImplementation(createAnalyticsServiceSetup), start: jest.fn().mockImplementation(createAnalyticsServiceStart), + stop: jest.fn(), }; }; diff --git a/src/core/server/analytics/analytics_service.ts b/src/core/server/analytics/analytics_service.ts index 4262a966ceb10..24389dfa7e938 100644 --- a/src/core/server/analytics/analytics_service.ts +++ b/src/core/server/analytics/analytics_service.ts @@ -8,6 +8,7 @@ import type { AnalyticsClient } from '@kbn/analytics-client'; import { createAnalytics } from '@kbn/analytics-client'; +import { of } from 'rxjs'; import type { CoreContext } from '../core_context'; /** @@ -15,13 +16,13 @@ import type { CoreContext } from '../core_context'; * {@link AnalyticsClient} * @public */ -export type AnalyticsServicePreboot = AnalyticsClient; +export type AnalyticsServicePreboot = Omit; /** * Exposes the public APIs of the AnalyticsClient during the setup phase. * {@link AnalyticsClient} * @public */ -export type AnalyticsServiceSetup = AnalyticsClient; +export type AnalyticsServiceSetup = Omit; /** * Exposes the public APIs of the AnalyticsClient during the start phase * {@link AnalyticsClient} @@ -43,6 +44,8 @@ export class AnalyticsService { // For now, we are relying on whether it's a distributable or running from source. sendTo: core.env.packageInfo.dist ? 'production' : 'staging', }); + + this.registerBuildInfoAnalyticsContext(core); } public preboot(): AnalyticsServicePreboot { @@ -74,4 +77,44 @@ export class AnalyticsService { telemetryCounter$: this.analyticsClient.telemetryCounter$, }; } + + public stop() { + this.analyticsClient.shutdown(); + } + + /** + * Enriches the event with the build information. + * @param core The core context. + * @private + */ + private registerBuildInfoAnalyticsContext(core: CoreContext) { + this.analyticsClient.registerContextProvider({ + name: 'build info', + context$: of({ + isDev: core.env.mode.dev, + isDistributable: core.env.packageInfo.dist, + version: core.env.packageInfo.version, + branch: core.env.packageInfo.branch, + buildNum: core.env.packageInfo.buildNum, + buildSha: core.env.packageInfo.buildSha, + }), + schema: { + isDev: { + type: 'boolean', + _meta: { description: 'Is it running in development mode?' }, + }, + isDistributable: { + type: 'boolean', + _meta: { description: 'Is it running from a distributable?' }, + }, + version: { type: 'keyword', _meta: { description: 'Version of the Kibana instance.' } }, + branch: { + type: 'keyword', + _meta: { description: 'Branch of source running Kibana from.' }, + }, + buildNum: { type: 'long', _meta: { description: 'Build number of the Kibana instance.' } }, + buildSha: { type: 'keyword', _meta: { description: 'Build SHA of the Kibana instance.' } }, + }, + }); + } } diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index 3ef44e2690a95..02a846a5b8011 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -25,6 +25,7 @@ import { } from './types'; import { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { ServiceStatus, ServiceStatusLevels } from '../status'; +import type { ClusterInfo } from './get_cluster_info'; type MockedElasticSearchServicePreboot = jest.Mocked; @@ -89,6 +90,11 @@ const createInternalSetupContractMock = () => { warningNodes: [], kibanaVersion: '8.0.0', }), + clusterInfo$: new BehaviorSubject({ + cluster_uuid: 'cluster-uuid', + cluster_name: 'cluster-name', + cluster_version: '8.0.0', + }), status$: new BehaviorSubject>({ level: ServiceStatusLevels.available, summary: 'Elasticsearch is available', diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index def2c400258b5..875995cd7cd96 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -34,6 +34,7 @@ import { elasticsearchClientMock } from './client/mocks'; import { duration } from 'moment'; import { isValidConnection as isValidConnectionMock } from './is_valid_connection'; import { pollEsNodesVersion as pollEsNodesVersionMocked } from './version_check/ensure_es_version'; +import { analyticsServiceMock } from '../analytics/analytics_service.mock'; const { pollEsNodesVersion: pollEsNodesVersionActual } = jest.requireActual( './version_check/ensure_es_version' @@ -53,6 +54,7 @@ let setupDeps: SetupDeps; beforeEach(() => { setupDeps = { + analytics: analyticsServiceMock.createAnalyticsServiceSetup(), http: httpServiceMock.createInternalSetupContract(), executionContext: executionContextServiceMock.createInternalSetupContract(), }; diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index d0cf23c539416..09e8b3172c8e7 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -9,6 +9,8 @@ import { firstValueFrom, Observable, Subject } from 'rxjs'; import { map, shareReplay, takeUntil } from 'rxjs/operators'; +import { registerAnalyticsContextProvider } from './register_analytics_context_provider'; +import { AnalyticsServiceSetup } from '../analytics'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; @@ -29,8 +31,10 @@ import { isValidConnection } from './is_valid_connection'; import { isInlineScriptingEnabled } from './is_scripting_enabled'; import type { UnauthorizedErrorHandler } from './client/retry_unauthorized'; import { mergeConfig } from './merge_config'; +import { getClusterInfo$ } from './get_cluster_info'; export interface SetupDeps { + analytics: AnalyticsServiceSetup; http: InternalHttpServiceSetup; executionContext: InternalExecutionContextSetup; } @@ -92,10 +96,14 @@ export class ElasticsearchService this.esNodesCompatibility$ = esNodesCompatibility$; + const clusterInfo$ = getClusterInfo$(this.client.asInternalUser); + registerAnalyticsContextProvider(deps.analytics, clusterInfo$); + return { legacy: { config$: this.config$, }, + clusterInfo$, esNodesCompatibility$, status$: calculateStatus$(esNodesCompatibility$), setUnauthorizedErrorHandler: (handler) => { diff --git a/src/core/server/elasticsearch/get_cluster_info.test.ts b/src/core/server/elasticsearch/get_cluster_info.test.ts new file mode 100644 index 0000000000000..fd3b3b71844ac --- /dev/null +++ b/src/core/server/elasticsearch/get_cluster_info.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { elasticsearchClientMock } from './client/mocks'; +import { firstValueFrom } from 'rxjs'; +import { getClusterInfo$ } from './get_cluster_info'; + +describe('getClusterInfo', () => { + let internalClient: ReturnType; + const infoResponse = { + cluster_name: 'cluster-name', + cluster_uuid: 'cluster_uuid', + name: 'name', + tagline: 'tagline', + version: { + number: '1.2.3', + lucene_version: '1.2.3', + build_date: 'DateString', + build_flavor: 'string', + build_hash: 'string', + build_snapshot: true, + build_type: 'string', + minimum_index_compatibility_version: '1.2.3', + minimum_wire_compatibility_version: '1.2.3', + }, + }; + + beforeEach(() => { + internalClient = elasticsearchClientMock.createInternalClient(); + }); + + test('it provides the context', async () => { + internalClient.info.mockResolvedValue(infoResponse); + const context$ = getClusterInfo$(internalClient); + await expect(firstValueFrom(context$)).resolves.toMatchInlineSnapshot(` + Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster_uuid", + "cluster_version": "1.2.3", + } + `); + }); + + test('it retries if it fails to fetch the cluster info', async () => { + internalClient.info.mockRejectedValueOnce(new Error('Failed to fetch cluster info')); + internalClient.info.mockResolvedValue(infoResponse); + const context$ = getClusterInfo$(internalClient); + await expect(firstValueFrom(context$)).resolves.toMatchInlineSnapshot(` + Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster_uuid", + "cluster_version": "1.2.3", + } + `); + expect(internalClient.info).toHaveBeenCalledTimes(2); + }); + + test('multiple subscribers do not trigger more ES requests', async () => { + internalClient.info.mockResolvedValue(infoResponse); + const context$ = getClusterInfo$(internalClient); + await expect(firstValueFrom(context$)).resolves.toMatchInlineSnapshot(` + Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster_uuid", + "cluster_version": "1.2.3", + } + `); + await expect(firstValueFrom(context$)).resolves.toMatchInlineSnapshot(` + Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster_uuid", + "cluster_version": "1.2.3", + } + `); + expect(internalClient.info).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/core/server/elasticsearch/get_cluster_info.ts b/src/core/server/elasticsearch/get_cluster_info.ts new file mode 100644 index 0000000000000..c807965d3bbf8 --- /dev/null +++ b/src/core/server/elasticsearch/get_cluster_info.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Observable } from 'rxjs'; +import { defer, map, retry, shareReplay } from 'rxjs'; +import type { ElasticsearchClient } from './client'; + +/** @private */ +export interface ClusterInfo { + cluster_name: string; + cluster_uuid: string; + cluster_version: string; +} + +/** + * Returns the cluster info from the Elasticsearch cluster. + * @param internalClient Elasticsearch client + * @private + */ +export function getClusterInfo$(internalClient: ElasticsearchClient): Observable { + return defer(() => internalClient.info()).pipe( + map((info) => ({ + cluster_name: info.cluster_name, + cluster_uuid: info.cluster_uuid, + cluster_version: info.version.number, + })), + retry({ delay: 1000 }), + shareReplay(1) + ); +} diff --git a/src/core/server/elasticsearch/register_analytics_context_provider.test.ts b/src/core/server/elasticsearch/register_analytics_context_provider.test.ts new file mode 100644 index 0000000000000..4f09ea8677f44 --- /dev/null +++ b/src/core/server/elasticsearch/register_analytics_context_provider.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { firstValueFrom, of } from 'rxjs'; +import type { AnalyticsServiceSetup } from '../analytics'; +import { analyticsServiceMock } from '../analytics/analytics_service.mock'; +import { registerAnalyticsContextProvider } from './register_analytics_context_provider'; + +describe('registerAnalyticsContextProvider', () => { + let analyticsMock: jest.Mocked; + + beforeEach(() => { + analyticsMock = analyticsServiceMock.createAnalyticsServiceSetup(); + }); + + test('it provides the context', async () => { + registerAnalyticsContextProvider( + analyticsMock, + of({ cluster_name: 'cluster-name', cluster_uuid: 'cluster_uuid', cluster_version: '1.2.3' }) + ); + const { context$ } = analyticsMock.registerContextProvider.mock.calls[0][0]; + await expect(firstValueFrom(context$)).resolves.toMatchInlineSnapshot(` + Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster_uuid", + "cluster_version": "1.2.3", + } + `); + }); +}); diff --git a/src/core/server/elasticsearch/register_analytics_context_provider.ts b/src/core/server/elasticsearch/register_analytics_context_provider.ts new file mode 100644 index 0000000000000..cc4523c0d4eb5 --- /dev/null +++ b/src/core/server/elasticsearch/register_analytics_context_provider.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Observable } from 'rxjs'; +import type { AnalyticsServiceSetup } from '../analytics'; +import type { ClusterInfo } from './get_cluster_info'; + +/** + * Registers the Analytics context provider to enrich events with the cluster info. + * @param analytics Analytics service. + * @param context$ Observable emitting the cluster info. + * @private + */ +export function registerAnalyticsContextProvider( + analytics: AnalyticsServiceSetup, + context$: Observable +) { + analytics.registerContextProvider({ + name: 'elasticsearch info', + context$, + schema: { + cluster_name: { type: 'keyword', _meta: { description: 'The Cluster Name' } }, + cluster_uuid: { type: 'keyword', _meta: { description: 'The Cluster UUID' } }, + cluster_version: { type: 'keyword', _meta: { description: 'The Cluster version' } }, + }, + }); +} diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 1f363804b3a33..12ba2575d2726 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -14,6 +14,7 @@ import { IClusterClient, ICustomClusterClient, ElasticsearchClientConfig } from import { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { ServiceStatus } from '../status'; import type { UnauthorizedErrorHandler } from './client/retry_unauthorized'; +import { ClusterInfo } from './get_cluster_info'; /** * @public @@ -97,6 +98,7 @@ export type InternalElasticsearchServicePreboot = ElasticsearchServicePreboot; /** @internal */ export interface InternalElasticsearchServiceSetup extends ElasticsearchServiceSetup { + clusterInfo$: Observable; esNodesCompatibility$: Observable; status$: Observable>; } diff --git a/src/core/server/environment/environment_service.test.ts b/src/core/server/environment/environment_service.test.ts index 0817fad35f882..c285edc443ce8 100644 --- a/src/core/server/environment/environment_service.test.ts +++ b/src/core/server/environment/environment_service.test.ts @@ -13,10 +13,12 @@ import { resolveInstanceUuid } from './resolve_uuid'; import { createDataFolder } from './create_data_folder'; import { writePidFile } from './write_pid_file'; import { CoreContext } from '../core_context'; +import type { AnalyticsServicePreboot } from '../analytics'; import { configServiceMock } from '../config/mocks'; import { loggingSystemMock } from '../logging/logging_system.mock'; import { mockCoreContext } from '../core_context.mock'; +import { analyticsServiceMock } from '../analytics/analytics_service.mock'; jest.mock('./resolve_uuid', () => ({ resolveInstanceUuid: jest.fn().mockResolvedValue('SOME_UUID'), @@ -63,11 +65,13 @@ describe('UuidService', () => { let configService: ReturnType; let coreContext: CoreContext; let service: EnvironmentService; + let analytics: AnalyticsServicePreboot; beforeEach(async () => { logger = loggingSystemMock.create(); configService = getConfigService(); coreContext = mockCoreContext.create({ logger, configService }); + analytics = analyticsServiceMock.createAnalyticsServicePreboot(); service = new EnvironmentService(coreContext); }); @@ -78,7 +82,7 @@ describe('UuidService', () => { describe('#preboot()', () => { it('calls resolveInstanceUuid with correct parameters', async () => { - await service.preboot(); + await service.preboot({ analytics }); expect(resolveInstanceUuid).toHaveBeenCalledTimes(1); expect(resolveInstanceUuid).toHaveBeenCalledWith({ @@ -89,7 +93,7 @@ describe('UuidService', () => { }); it('calls createDataFolder with correct parameters', async () => { - await service.preboot(); + await service.preboot({ analytics }); expect(createDataFolder).toHaveBeenCalledTimes(1); expect(createDataFolder).toHaveBeenCalledWith({ @@ -99,7 +103,7 @@ describe('UuidService', () => { }); it('calls writePidFile with correct parameters', async () => { - await service.preboot(); + await service.preboot({ analytics }); expect(writePidFile).toHaveBeenCalledTimes(1); expect(writePidFile).toHaveBeenCalledWith({ @@ -109,14 +113,14 @@ describe('UuidService', () => { }); it('returns the uuid resolved from resolveInstanceUuid', async () => { - const preboot = await service.preboot(); + const preboot = await service.preboot({ analytics }); expect(preboot.instanceUuid).toEqual('SOME_UUID'); }); describe('process warnings', () => { it('logs warnings coming from the process', async () => { - await service.preboot(); + await service.preboot({ analytics }); const warning = new Error('something went wrong'); process.emit('warning', warning); @@ -126,7 +130,7 @@ describe('UuidService', () => { }); it('does not log deprecation warnings', async () => { - await service.preboot(); + await service.preboot({ analytics }); const warning = new Error('something went wrong'); warning.name = 'DeprecationWarning'; @@ -139,7 +143,7 @@ describe('UuidService', () => { // TODO: From Nodejs v16 emitting an unhandledRejection will kill the process describe.skip('unhandledRejection warnings', () => { it('logs warn for an unhandeld promise rejected with an Error', async () => { - await service.preboot(); + await service.preboot({ analytics }); const err = new Error('something went wrong'); process.emit('unhandledRejection', err, new Promise((res, rej) => rej(err))); @@ -151,7 +155,7 @@ describe('UuidService', () => { }); it('logs warn for an unhandeld promise rejected with a string', async () => { - await service.preboot(); + await service.preboot({ analytics }); const err = 'something went wrong'; process.emit('unhandledRejection', err, new Promise((res, rej) => rej(err))); @@ -166,7 +170,7 @@ describe('UuidService', () => { describe('#setup()', () => { it('returns the uuid resolved from resolveInstanceUuid', async () => { - await expect(service.preboot()).resolves.toEqual({ instanceUuid: 'SOME_UUID' }); + await expect(service.preboot({ analytics })).resolves.toEqual({ instanceUuid: 'SOME_UUID' }); expect(service.setup()).toEqual({ instanceUuid: 'SOME_UUID' }); }); }); diff --git a/src/core/server/environment/environment_service.ts b/src/core/server/environment/environment_service.ts index 65c03b108b28a..28e2da446eb95 100644 --- a/src/core/server/environment/environment_service.ts +++ b/src/core/server/environment/environment_service.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { firstValueFrom } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { PathConfigType, config as pathConfigDef } from '@kbn/utils'; +import type { AnalyticsServicePreboot } from '../analytics'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { IConfigService } from '../config'; @@ -17,6 +18,16 @@ import { resolveInstanceUuid } from './resolve_uuid'; import { createDataFolder } from './create_data_folder'; import { writePidFile } from './write_pid_file'; +/** + * @internal + */ +export interface PrebootDeps { + /** + * {@link AnalyticsServicePreboot} + */ + analytics: AnalyticsServicePreboot; +} + /** * @internal */ @@ -45,7 +56,7 @@ export class EnvironmentService { this.configService = core.configService; } - public async preboot() { + public async preboot({ analytics }: PrebootDeps) { // IMPORTANT: This code is based on the assumption that none of the configuration values used // here is supposed to change during preboot phase and it's safe to read them only once. const [pathConfig, serverConfig, pidConfig] = await Promise.all([ @@ -77,6 +88,24 @@ export class EnvironmentService { logger: this.log, }); + analytics.registerContextProvider({ + name: 'kibana info', + context$: of({ + kibana_uuid: this.uuid, + pid: process.pid, + }), + schema: { + kibana_uuid: { + type: 'keyword', + _meta: { description: 'Kibana instance UUID' }, + }, + pid: { + type: 'long', + _meta: { description: 'Process ID' }, + }, + }, + }); + return { instanceUuid: this.uuid, }; diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index f251d3fb64cab..557a10da0839d 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -45,8 +45,9 @@ export type HttpServiceSetupMock = jest.Mocked< createRouter: jest.MockedFunction<() => RouterMock>; }; export type InternalHttpServiceSetupMock = jest.Mocked< - Omit + Omit > & { + auth: AuthMocked; basePath: BasePathMocked; createRouter: jest.MockedFunction<(path: string) => RouterMock>; authRequestHeaders: jest.Mocked; diff --git a/src/core/server/rendering/__mocks__/params.ts b/src/core/server/rendering/__mocks__/params.ts index 091d185cceefc..b4ead2e628688 100644 --- a/src/core/server/rendering/__mocks__/params.ts +++ b/src/core/server/rendering/__mocks__/params.ts @@ -7,6 +7,7 @@ */ import { mockCoreContext } from '../../core_context.mock'; +import { elasticsearchServiceMock } from '../../elasticsearch/elasticsearch_service.mock'; import { httpServiceMock } from '../../http/http_service.mock'; import { pluginServiceMock } from '../../plugins/plugins_service.mock'; import { statusServiceMock } from '../../status/status_service.mock'; @@ -15,6 +16,7 @@ const context = mockCoreContext.create(); const httpPreboot = httpServiceMock.createInternalPrebootContract(); const httpSetup = httpServiceMock.createInternalSetupContract(); const status = statusServiceMock.createInternalSetupContract(); +const elasticsearch = elasticsearchServiceMock.createInternalSetup(); export const mockRenderingServiceParams = context; export const mockRenderingPrebootDeps = { @@ -22,6 +24,7 @@ export const mockRenderingPrebootDeps = { uiPlugins: pluginServiceMock.createUiPlugins(), }; export const mockRenderingSetupDeps = { + elasticsearch, http: httpSetup, uiPlugins: pluginServiceMock.createUiPlugins(), status, diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index 4abf24911808c..9fe0cb545e7aa 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -6,6 +6,7 @@ Object { "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, + "clusterInfo": Object {}, "csp": Object { "warnLegacyBrowsers": true, }, @@ -61,6 +62,7 @@ Object { "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, + "clusterInfo": Object {}, "csp": Object { "warnLegacyBrowsers": true, }, @@ -120,6 +122,7 @@ Object { "basePath": "", "branch": Any, "buildNumber": Any, + "clusterInfo": Object {}, "csp": Object { "warnLegacyBrowsers": true, }, @@ -169,12 +172,69 @@ Object { } `; +exports[`RenderingService preboot() render() renders "core" page for unauthenticated requests 1`] = ` +Object { + "anonymousStatusPage": false, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "clusterInfo": Object {}, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "mode": Object { + "dev": Any, + "name": Any, + "prod": Any, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": Any, + "version": Any, + }, + }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", + "serverBasePath": "/mock-server-basepath", + "theme": Object { + "darkMode": "theme:darkMode", + "version": "v8", + }, + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + exports[`RenderingService preboot() render() renders "core" with excluded user settings 1`] = ` Object { "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, + "clusterInfo": Object {}, "csp": Object { "warnLegacyBrowsers": true, }, @@ -230,6 +290,7 @@ Object { "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, + "clusterInfo": Object {}, "csp": Object { "warnLegacyBrowsers": true, }, @@ -285,6 +346,11 @@ Object { "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, + "clusterInfo": Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster-uuid", + "cluster_version": "8.0.0", + }, "csp": Object { "warnLegacyBrowsers": true, }, @@ -344,6 +410,11 @@ Object { "basePath": "", "branch": Any, "buildNumber": Any, + "clusterInfo": Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster-uuid", + "cluster_version": "8.0.0", + }, "csp": Object { "warnLegacyBrowsers": true, }, @@ -393,12 +464,73 @@ Object { } `; +exports[`RenderingService setup() render() renders "core" page for unauthenticated requests 1`] = ` +Object { + "anonymousStatusPage": false, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "clusterInfo": Object {}, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "mode": Object { + "dev": Any, + "name": Any, + "prod": Any, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": Any, + "version": Any, + }, + }, + "externalUrl": Object { + "policy": Array [ + Object { + "allow": true, + }, + ], + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + }, + "publicBaseUrl": "http://myhost.com/mock-server-basepath", + "serverBasePath": "/mock-server-basepath", + "theme": Object { + "darkMode": "theme:darkMode", + "version": "v8", + }, + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + exports[`RenderingService setup() render() renders "core" with excluded user settings 1`] = ` Object { "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, + "clusterInfo": Object { + "cluster_name": "cluster-name", + "cluster_uuid": "cluster-uuid", + "cluster_version": "8.0.0", + }, "csp": Object { "warnLegacyBrowsers": true, }, diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts index cb10d01e85773..8aecc536d8846 100644 --- a/src/core/server/rendering/rendering_service.test.ts +++ b/src/core/server/rendering/rendering_service.test.ts @@ -25,6 +25,7 @@ import { } from './__mocks__/params'; import { InternalRenderingServicePreboot, InternalRenderingServiceSetup } from './types'; import { RenderingService } from './rendering_service'; +import { AuthStatus } from '../http/auth_state_storage'; const INJECTED_METADATA = { version: expect.any(String), @@ -75,6 +76,23 @@ function renderTestCases( expect(data).toMatchSnapshot(INJECTED_METADATA); }); + it('renders "core" page for unauthenticated requests', async () => { + mockRenderingSetupDeps.http.auth.get.mockReturnValueOnce({ + status: AuthStatus.unauthenticated, + state: {}, + }); + + const [render] = await getRender(); + const content = await render( + createKibanaRequest({ auth: { isAuthenticated: false } }), + uiSettings + ); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""'); + + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); + it('renders "core" page for blank basepath', async () => { const [render, deps] = await getRender(); deps.http.basePath.get.mockReturnValueOnce(''); diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 73746a8f202ff..3e50aac6fcbdd 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -8,10 +8,11 @@ import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; -import { take } from 'rxjs/operators'; +import { catchError, take, timeout } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import type { ThemeVersion } from '@kbn/ui-shared-deps-npm'; +import { firstValueFrom, of } from 'rxjs'; import type { UiPlugins } from '../plugins'; import { CoreContext } from '../core_context'; import { Template } from './views'; @@ -25,11 +26,13 @@ import { } from './types'; import { registerBootstrapRoute, bootstrapRendererFactory } from './bootstrap'; import { getSettingValue, getStylesheetPaths } from './render_utils'; -import { KibanaRequest } from '../http'; +import type { HttpAuth, KibanaRequest } from '../http'; import { IUiSettingsClient } from '../ui_settings'; import { filterUiPlugins } from './filter_ui_plugins'; -type RenderOptions = (RenderingPrebootDeps & { status?: never }) | RenderingSetupDeps; +type RenderOptions = + | (RenderingPrebootDeps & { status?: never; elasticsearch?: never }) + | RenderingSetupDeps; /** @internal */ export class RenderingService { @@ -57,6 +60,7 @@ export class RenderingService { } public async setup({ + elasticsearch, http, status, uiPlugins, @@ -72,12 +76,12 @@ export class RenderingService { }); return { - render: this.render.bind(this, { http, uiPlugins, status }), + render: this.render.bind(this, { elasticsearch, http, uiPlugins, status }), }; } private async render( - { http, uiPlugins, status }: RenderOptions, + { elasticsearch, http, uiPlugins, status }: RenderOptions, request: KibanaRequest, uiSettings: IUiSettingsClient, { isAnonymousPage = false, vars, includeExposedConfigKeys }: IRenderOptions = {} @@ -94,6 +98,21 @@ export class RenderingService { user: isAnonymousPage ? {} : await uiSettings.getUserProvided(), }; + let clusterInfo = {}; + try { + // Only provide the clusterInfo if the request is authenticated and the elasticsearch service is available. + if (isAuthenticated(http.auth, request) && elasticsearch) { + clusterInfo = await firstValueFrom( + elasticsearch.clusterInfo$.pipe( + timeout(50), // If not available, just return undefined + catchError(() => of({})) + ) + ); + } + } catch (err) { + // swallow error + } + const darkMode = getSettingValue('theme:darkMode', settings, Boolean); const themeVersion: ThemeVersion = 'v8'; @@ -123,6 +142,7 @@ export class RenderingService { serverBasePath, publicBaseUrl, env, + clusterInfo, anonymousStatusPage: status?.isStatusPageAnonymous() ?? false, i18n: { translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`, @@ -164,3 +184,9 @@ const getUiConfig = async (uiPlugins: UiPlugins, pluginId: string) => { exposedConfigKeys: {}, }) as { browserConfig: Record; exposedConfigKeys: Record }; }; + +const isAuthenticated = (auth: HttpAuth, request: KibanaRequest) => { + const { status: authStatus } = auth.get(request); + // status is 'unknown' when auth is disabled. we just need to not be `unauthenticated` here. + return authStatus !== 'unauthenticated'; +}; diff --git a/src/core/server/rendering/types.ts b/src/core/server/rendering/types.ts index 2c0aafe61e018..82758018b859d 100644 --- a/src/core/server/rendering/types.ts +++ b/src/core/server/rendering/types.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import type { ThemeVersion } from '@kbn/ui-shared-deps-npm'; +import { InternalElasticsearchServiceSetup } from '../elasticsearch'; import { EnvironmentMode, PackageInfo } from '../config'; import { ICspConfig } from '../csp'; import { InternalHttpServicePreboot, InternalHttpServiceSetup, KibanaRequest } from '../http'; @@ -38,6 +39,11 @@ export interface InjectedMetadata { basePath: string; serverBasePath: string; publicBaseUrl?: string; + clusterInfo: { + cluster_uuid?: string; + cluster_name?: string; + cluster_version?: string; + }; env: { mode: EnvironmentMode; packageInfo: PackageInfo; @@ -74,6 +80,7 @@ export interface RenderingPrebootDeps { /** @internal */ export interface RenderingSetupDeps { + elasticsearch: InternalElasticsearchServiceSetup; http: InternalHttpServiceSetup; status: InternalStatusServiceSetup; uiPlugins: UiPlugins; diff --git a/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap index d26021d28b0e5..e6e1fc2cdc21d 100644 --- a/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap +++ b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap @@ -139,6 +139,11 @@ Object { "type": "tsvb-validation-telemetry", }, }, + Object { + "term": Object { + "type": "ui-counter", + }, + }, Object { "bool": Object { "must": Array [ @@ -305,6 +310,11 @@ Object { "type": "tsvb-validation-telemetry", }, }, + Object { + "term": Object { + "type": "ui-counter", + }, + }, Object { "bool": Object { "must": Array [ @@ -475,6 +485,11 @@ Object { "type": "tsvb-validation-telemetry", }, }, + Object { + "term": Object { + "type": "ui-counter", + }, + }, Object { "bool": Object { "must": Array [ @@ -649,6 +664,11 @@ Object { "type": "tsvb-validation-telemetry", }, }, + Object { + "term": Object { + "type": "ui-counter", + }, + }, Object { "bool": Object { "must": Array [ @@ -860,6 +880,11 @@ Object { "type": "tsvb-validation-telemetry", }, }, + Object { + "term": Object { + "type": "ui-counter", + }, + }, Object { "bool": Object { "must": Array [ @@ -1037,6 +1062,11 @@ Object { "type": "tsvb-validation-telemetry", }, }, + Object { + "term": Object { + "type": "ui-counter", + }, + }, Object { "bool": Object { "must": Array [ diff --git a/src/core/server/saved_objects/migrations/core/unused_types.ts b/src/core/server/saved_objects/migrations/core/unused_types.ts index fd4b8a09600d7..076bdb489cf49 100644 --- a/src/core/server/saved_objects/migrations/core/unused_types.ts +++ b/src/core/server/saved_objects/migrations/core/unused_types.ts @@ -33,6 +33,8 @@ export const REMOVED_TYPES: string[] = [ 'siem-detection-engine-rule-status', // Was removed in 7.16 'timelion-sheet', + // Removed in 8.3 https://github.com/elastic/kibana/issues/127745 + 'ui-counter', ].sort(); // When migrating from the outdated index we use a read query which excludes diff --git a/src/core/server/saved_objects/migrations/integration_tests/cluster_routing_allocation_disabled.test.ts b/src/core/server/saved_objects/migrations/integration_tests/cluster_routing_allocation_disabled.test.ts index 37b278fe9ccf0..525b9b3585c3f 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/cluster_routing_allocation_disabled.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/cluster_routing_allocation_disabled.test.ts @@ -114,7 +114,7 @@ describe('unsupported_cluster_routing_allocation', () => { await root.setup(); await expect(root.start()).rejects.toThrowError( - /Unable to complete saved object migrations for the \[\.kibana\] index: \[unsupported_cluster_routing_allocation\] The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue\. To proceed, please remove the cluster routing allocation settings with PUT \/_cluster\/settings {\"transient\": {\"cluster\.routing\.allocation\.enable\": null}, \"persistent\": {\"cluster\.routing\.allocation\.enable\": null}}\. Refer to https:\/\/www.elastic.co\/guide\/en\/kibana\/master\/resolve-migrations-failures.html#routing-allocation-disabled for more information on how to resolve the issue\./ + /Unable to complete saved object migrations for the \[\.kibana.*\] index: \[unsupported_cluster_routing_allocation\] The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue\. To proceed, please remove the cluster routing allocation settings with PUT \/_cluster\/settings {\"transient\": {\"cluster\.routing\.allocation\.enable\": null}, \"persistent\": {\"cluster\.routing\.allocation\.enable\": null}}\. Refer to https:\/\/www.elastic.co\/guide\/en\/kibana\/master\/resolve-migrations-failures.html#routing-allocation-disabled for more information on how to resolve the issue\./ ); await retryAsync( @@ -149,7 +149,7 @@ describe('unsupported_cluster_routing_allocation', () => { await root.setup(); await expect(root.start()).rejects.toThrowError( - /Unable to complete saved object migrations for the \[\.kibana\] index: \[unsupported_cluster_routing_allocation\] The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue\. To proceed, please remove the cluster routing allocation settings with PUT \/_cluster\/settings {\"transient\": {\"cluster\.routing\.allocation\.enable\": null}, \"persistent\": {\"cluster\.routing\.allocation\.enable\": null}}\. Refer to https:\/\/www.elastic.co\/guide\/en\/kibana\/master\/resolve-migrations-failures.html#routing-allocation-disabled for more information on how to resolve the issue\./ + /Unable to complete saved object migrations for the \[\.kibana.*\] index: \[unsupported_cluster_routing_allocation\] The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue\. To proceed, please remove the cluster routing allocation settings with PUT \/_cluster\/settings {\"transient\": {\"cluster\.routing\.allocation\.enable\": null}, \"persistent\": {\"cluster\.routing\.allocation\.enable\": null}}\. Refer to https:\/\/www.elastic.co\/guide\/en\/kibana\/master\/resolve-migrations-failures.html#routing-allocation-disabled for more information on how to resolve the issue\./ ); }); }); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ea83e210cc4e1..4ff42f95b571a 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -77,12 +77,12 @@ export { AnalyticsClient } // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver // // @public -export type AnalyticsServicePreboot = AnalyticsClient; +export type AnalyticsServicePreboot = Omit; // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver // // @public -export type AnalyticsServiceSetup = AnalyticsClient; +export type AnalyticsServiceSetup = Omit; // Warning: (ae-unresolved-link) The @link reference could not be resolved: This type of declaration is not supported yet by the resolver // diff --git a/src/core/server/server.ts b/src/core/server/server.ts index c73e98f4bb6c4..234630734d437 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -56,10 +56,25 @@ import { config as executionContextConfig } from './execution_context'; import { PrebootCoreRouteHandlerContext } from './preboot_core_route_handler_context'; import { PrebootService } from './preboot'; import { DiscoveredPlugins } from './plugins'; -import { AnalyticsService } from './analytics'; +import { AnalyticsService, AnalyticsServiceSetup } from './analytics'; const coreId = Symbol('core'); const rootConfigPath = ''; +const KIBANA_STARTED_EVENT = 'kibana_started'; + +/** @internal */ +interface UptimePerStep { + start: number; + end: number; +} + +/** @internal */ +interface UptimeSteps { + constructor: UptimePerStep; + preboot: UptimePerStep; + setup: UptimePerStep; + start: UptimePerStep; +} export class Server { public readonly configService: ConfigService; @@ -94,11 +109,15 @@ export class Server { private discoveredPlugins?: DiscoveredPlugins; private readonly logger: LoggerFactory; + private readonly uptimePerStep: Partial = {}; + constructor( rawConfigProvider: RawConfigurationProvider, public readonly env: Env, private readonly loggingSystem: ILoggingSystem ) { + const constructorStartUptime = process.uptime(); + this.logger = this.loggingSystem.asLoggerFactory(); this.log = this.logger.get('server'); this.configService = new ConfigService(rawConfigProvider, env, this.logger); @@ -129,15 +148,18 @@ export class Server { this.savedObjectsStartPromise = new Promise((resolve) => { this.resolveSavedObjectsStartPromise = resolve; }); + + this.uptimePerStep.constructor = { start: constructorStartUptime, end: process.uptime() }; } public async preboot() { this.log.debug('prebooting server'); + const prebootStartUptime = process.uptime(); const prebootTransaction = apm.startTransaction('server-preboot', 'kibana-platform'); const analyticsPreboot = this.analytics.preboot(); - const environmentPreboot = await this.environment.preboot(); + const environmentPreboot = await this.environment.preboot({ analytics: analyticsPreboot }); // Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph. this.discoveredPlugins = await this.plugins.discover({ environment: environmentPreboot }); @@ -187,15 +209,19 @@ export class Server { this.coreApp.preboot(corePreboot, uiPlugins); prebootTransaction?.end(); + this.uptimePerStep.preboot = { start: prebootStartUptime, end: process.uptime() }; return corePreboot; } public async setup() { this.log.debug('setting up server'); + const setupStartUptime = process.uptime(); const setupTransaction = apm.startTransaction('server-setup', 'kibana-platform'); const analyticsSetup = this.analytics.setup(); + this.registerKibanaStartedEventType(analyticsSetup); + const environmentSetup = this.environment.setup(); // Configuration could have changed after preboot. @@ -223,6 +249,7 @@ export class Server { const capabilitiesSetup = this.capabilities.setup({ http: httpSetup }); const elasticsearchServiceSetup = await this.elasticsearch.setup({ + analytics: analyticsSetup, http: httpSetup, executionContext: executionContextSetup, }); @@ -249,6 +276,7 @@ export class Server { }); const statusSetup = await this.status.setup({ + analytics: analyticsSetup, elasticsearch: elasticsearchServiceSetup, pluginDependencies: pluginTree.asNames, savedObjects: savedObjectsSetup, @@ -259,6 +287,7 @@ export class Server { }); const renderingSetup = await this.rendering.setup({ + elasticsearch: elasticsearchServiceSetup, http: httpSetup, status: statusSetup, uiPlugins, @@ -299,11 +328,13 @@ export class Server { this.coreApp.setup(coreSetup, uiPlugins); setupTransaction?.end(); + this.uptimePerStep.setup = { start: setupStartUptime, end: process.uptime() }; return coreSetup; } public async start() { this.log.debug('starting server'); + const startStartUptime = process.uptime(); const startTransaction = apm.startTransaction('server-start', 'kibana-platform'); const analyticsStart = this.analytics.start(); @@ -352,12 +383,16 @@ export class Server { startTransaction?.end(); + this.uptimePerStep.start = { start: startStartUptime, end: process.uptime() }; + analyticsStart.reportEvent(KIBANA_STARTED_EVENT, { uptime_per_step: this.uptimePerStep }); + return this.coreStart; } public async stop() { this.log.debug('stopping server'); + this.analytics.stop(); await this.http.stop(); // HTTP server has to stop before savedObjects and ES clients are closed to be able to gracefully attempt to resolve any pending requests await this.plugins.stop(); await this.savedObjects.stop(); @@ -404,4 +439,92 @@ export class Server { this.configService.setSchema(descriptor.path, descriptor.schema); } } + + private registerKibanaStartedEventType(analyticsSetup: AnalyticsServiceSetup) { + analyticsSetup.registerEventType<{ uptime_per_step: UptimeSteps }>({ + eventType: KIBANA_STARTED_EVENT, + schema: { + uptime_per_step: { + properties: { + constructor: { + properties: { + start: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until the constructor was called', + }, + }, + end: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until the constructor finished', + }, + }, + }, + }, + preboot: { + properties: { + start: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `preboot` was called', + }, + }, + end: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `preboot` finished', + }, + }, + }, + }, + setup: { + properties: { + start: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `setup` was called', + }, + }, + end: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `setup` finished', + }, + }, + }, + }, + start: { + properties: { + start: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `start` was called', + }, + }, + end: { + type: 'float', + _meta: { + description: + 'Number of seconds the Node.js process has been running until `start` finished', + }, + }, + }, + }, + }, + _meta: { + description: + 'Number of seconds the Node.js process has been running until each phase of the server execution is called and finished.', + }, + }, + }, + }); + } } diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts index 262667fddf26a..70181db9380ff 100644 --- a/src/core/server/status/status_service.test.ts +++ b/src/core/server/status/status_service.test.ts @@ -6,11 +6,16 @@ * Side Public License, v 1. */ -import { of, BehaviorSubject } from 'rxjs'; - -import { ServiceStatus, ServiceStatusLevels, CoreStatus } from './types'; +import { of, BehaviorSubject, firstValueFrom } from 'rxjs'; + +import { + ServiceStatus, + ServiceStatusLevels, + CoreStatus, + InternalStatusServiceSetup, +} from './types'; import { StatusService } from './status_service'; -import { first } from 'rxjs/operators'; +import { first, take, toArray } from 'rxjs/operators'; import { mockCoreContext } from '../core_context.mock'; import { ServiceStatusLevelSnapshotSerializer } from './test_utils'; import { environmentServiceMock } from '../environment/environment_service.mock'; @@ -19,6 +24,8 @@ import { mockRouter, RouterMock } from '../http/router/router.mock'; import { metricsServiceMock } from '../metrics/metrics_service.mock'; import { configServiceMock } from '../config/mocks'; import { coreUsageDataServiceMock } from '../core_usage_data/core_usage_data_service.mock'; +import { analyticsServiceMock } from '../analytics/analytics_service.mock'; +import { AnalyticsServiceSetup } from '..'; expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); @@ -47,6 +54,7 @@ describe('StatusService', () => { type SetupDeps = Parameters[0]; const setupDeps = (overrides: Partial): SetupDeps => { return { + analytics: analyticsServiceMock.createAnalyticsServiceSetup(), elasticsearch: { status$: of(available), }, @@ -535,5 +543,50 @@ describe('StatusService', () => { ); }); }); + + describe('analytics', () => { + let analyticsMock: jest.Mocked; + let setup: InternalStatusServiceSetup; + + beforeEach(async () => { + analyticsMock = analyticsServiceMock.createAnalyticsServiceSetup(); + setup = await service.setup(setupDeps({ analytics: analyticsMock })); + }); + + test('registers a context provider', async () => { + expect(analyticsMock.registerContextProvider).toHaveBeenCalledTimes(1); + const { context$ } = analyticsMock.registerContextProvider.mock.calls[0][0]; + await expect(firstValueFrom(context$.pipe(take(2), toArray()))).resolves + .toMatchInlineSnapshot(` + Array [ + Object { + "overall_status_level": "initializing", + "overall_status_summary": "Kibana is starting up", + }, + Object { + "overall_status_level": "available", + "overall_status_summary": "All services are available", + }, + ] + `); + }); + + test('registers and reports an event', async () => { + expect(analyticsMock.registerEventType).toHaveBeenCalledTimes(1); + expect(analyticsMock.reportEvent).toHaveBeenCalledTimes(0); + // wait for an emission of overall$ + await firstValueFrom(setup.overall$); + expect(analyticsMock.reportEvent).toHaveBeenCalledTimes(1); + expect(analyticsMock.reportEvent.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "core-overall_status_changed", + Object { + "overall_status_level": "available", + "overall_status_summary": "All services are available", + }, + ] + `); + }); + }); }); }); diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index 6c8f8716c036e..a3dc0335c88af 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -6,10 +6,21 @@ * Side Public License, v 1. */ -import { Observable, combineLatest, Subscription, Subject, firstValueFrom } from 'rxjs'; -import { map, distinctUntilChanged, shareReplay, debounceTime } from 'rxjs/operators'; +import { + Observable, + combineLatest, + Subscription, + Subject, + firstValueFrom, + tap, + BehaviorSubject, +} from 'rxjs'; +import { map, distinctUntilChanged, shareReplay, debounceTime, takeUntil } from 'rxjs/operators'; import { isDeepStrictEqual } from 'util'; +import type { RootSchema } from '@kbn/analytics-client'; + +import { AnalyticsServiceSetup } from '../analytics'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Logger, LogMeta } from '../logging'; @@ -32,7 +43,13 @@ interface StatusLogMeta extends LogMeta { kibana: { status: ServiceStatus }; } +interface StatusAnalyticsPayload { + overall_status_level: string; + overall_status_summary: string; +} + export interface SetupDeps { + analytics: AnalyticsServiceSetup; elasticsearch: Pick; environment: InternalEnvironmentServiceSetup; pluginDependencies: ReadonlyMap; @@ -57,6 +74,7 @@ export class StatusService implements CoreService { } public async setup({ + analytics, elasticsearch, pluginDependencies, http, @@ -88,6 +106,8 @@ export class StatusService implements CoreService { shareReplay(1) ); + this.setupAnalyticsContextAndEvents(analytics); + const coreOverall$ = core$.pipe( // Prevent many emissions at once from dependency status resolution from making this too noisy debounceTime(25), @@ -192,4 +212,40 @@ export class StatusService implements CoreService { shareReplay(1) ); } + + private setupAnalyticsContextAndEvents(analytics: AnalyticsServiceSetup) { + // Set an initial "initializing" status, so we can attach it to early events. + const context$ = new BehaviorSubject({ + overall_status_level: 'initializing', + overall_status_summary: 'Kibana is starting up', + }); + + // The schema is the same for the context and the events. + const schema: RootSchema = { + overall_status_level: { + type: 'keyword', + _meta: { description: 'The current availability level of the service.' }, + }, + overall_status_summary: { + type: 'text', + _meta: { description: 'A high-level summary of the service status.' }, + }, + }; + + const overallStatusChangedEventName = 'core-overall_status_changed'; + + analytics.registerEventType({ eventType: overallStatusChangedEventName, schema }); + analytics.registerContextProvider({ name: 'status info', context$, schema }); + + this.overall$!.pipe( + takeUntil(this.stop$), + map(({ level, summary }) => ({ + overall_status_level: level.toString(), + overall_status_summary: summary, + })), + // Emit the event before spreading the status to the context. + // This way we see from the context the previous status and the current one. + tap((statusPayload) => analytics.reportEvent(overallStatusChangedEventName, statusPayload)) + ).subscribe(context$); + } } diff --git a/src/core/types/execution_context.ts b/src/core/types/execution_context.ts index d790b8d855fd4..d1e5cd10e5e91 100644 --- a/src/core/types/execution_context.ts +++ b/src/core/types/execution_context.ts @@ -14,7 +14,7 @@ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type KibanaExecutionContext = { /** - * Kibana application initated an operation. + * Kibana application initiated an operation. * */ readonly type?: string; // 'visualization' | 'actions' | 'server' | ..; /** public name of an application or a user-facing feature */ diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index ad9c8323b769d..0a3db5dc36d07 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -80,10 +80,11 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions await run(Tasks.CreatePackageJson); await run(Tasks.InstallDependencies); await run(Tasks.GeneratePackagesOptimizedAssets); - await run(Tasks.CleanPackages); + await run(Tasks.DeleteBazelPackagesFromBuildRoot); await run(Tasks.CreateNoticeFile); await run(Tasks.UpdateLicenseFile); await run(Tasks.RemovePackageJsonDeps); + await run(Tasks.CleanPackageManagerRelatedFiles); await run(Tasks.CleanTypescript); await run(Tasks.CleanExtraFilesFromModules); await run(Tasks.CleanEmptyFolders); diff --git a/src/dev/build/tasks/build_packages_task.ts b/src/dev/build/tasks/build_packages_task.ts index e30ffd082e250..62baf74559a2a 100644 --- a/src/dev/build/tasks/build_packages_task.ts +++ b/src/dev/build/tasks/build_packages_task.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import cpy from 'cpy'; import Path from 'path'; import { discoverBazelPackages } from '@kbn/bazel-packages'; @@ -53,9 +54,9 @@ export const BuildXpack: Task = { }); log.info('copying built x-pack into build dir'); - await scanCopy({ - source: config.resolveFromRepo('x-pack/build/plugin/kibana/x-pack'), - destination: build.resolvePath('x-pack'), + await cpy('**/{.,}*', build.resolvePath('x-pack'), { + cwd: config.resolveFromRepo('x-pack/build/plugin/kibana/x-pack'), + parents: true, }); }, }; diff --git a/src/dev/build/tasks/clean_tasks.ts b/src/dev/build/tasks/clean_tasks.ts index 19747ce72b5a6..c794ca277f77f 100644 --- a/src/dev/build/tasks/clean_tasks.ts +++ b/src/dev/build/tasks/clean_tasks.ts @@ -7,7 +7,7 @@ */ import minimatch from 'minimatch'; - +import { discoverBazelPackages } from '@kbn/bazel-packages'; import { deleteAll, deleteEmptyFolders, scanDelete, Task, GlobalTask } from '../lib'; export const Clean: GlobalTask = { @@ -26,14 +26,11 @@ export const Clean: GlobalTask = { }, }; -export const CleanPackages: Task = { - description: 'Cleaning source for packages that are now installed in node_modules', +export const CleanPackageManagerRelatedFiles: Task = { + description: 'Cleaning package manager related files from the build folder', async run(config, log, build) { - await deleteAll( - [build.resolvePath('packages'), build.resolvePath('yarn.lock'), build.resolvePath('.npmrc')], - log - ); + await deleteAll([build.resolvePath('yarn.lock'), build.resolvePath('.npmrc')], log); }, }; @@ -200,3 +197,16 @@ export const CleanEmptyFolders: Task = { ]); }, }; + +export const DeleteBazelPackagesFromBuildRoot: Task = { + description: + 'Deleting bazel packages outputs from build folder root as they are now installed as node_modules', + + async run(config, log, build) { + const bazelPackagesOnBuildRoot = (await discoverBazelPackages()).map((pkg) => + build.resolvePath(pkg.normalizedRepoRelativeDir) + ); + + await deleteAll(bazelPackagesOnBuildRoot, log); + }, +}; diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index 141eafc57ae1f..9fc0827e8c2c6 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import { getAllRepoRelativeBazelPackageDirs } from '@kbn/bazel-packages'; -import normalizePath from 'normalize-path'; +import { discoverBazelPackages } from '@kbn/bazel-packages'; import { copyAll, Task } from '../lib'; @@ -48,8 +47,8 @@ export const CopySource: Task = { 'tsconfig*.json', '.i18nrc.json', 'kibana.d.ts', - // explicitly ignore all package roots, even if they're not selected by previous patterns - ...getAllRepoRelativeBazelPackageDirs().map((dir) => `!${normalizePath(dir)}/**`), + // explicitly ignore all bazel package locations, even if they're not selected by previous patterns + ...(await discoverBazelPackages()).map((pkg) => `!${pkg.normalizedRepoRelativeDir}/**`), ], }); }, diff --git a/src/dev/build/tasks/transpile_babel_task.ts b/src/dev/build/tasks/transpile_babel_task.ts index 37f63d31415e9..ee7d1e19de43a 100644 --- a/src/dev/build/tasks/transpile_babel_task.ts +++ b/src/dev/build/tasks/transpile_babel_task.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { discoverBazelPackages } from '@kbn/bazel-packages'; import { pipeline } from 'stream'; import { promisify } from 'util'; @@ -24,10 +25,10 @@ const transpileWithBabel = async (srcGlobs: string[], build: Build, preset: stri vfs.src( srcGlobs.concat([ '!**/*.d.ts', - '!packages/**', '!**/node_modules/**', '!**/bower_components/**', '!**/__tests__/**', + ...(await discoverBazelPackages()).map((pkg) => `!${pkg.normalizedRepoRelativeDir}/**`), ]), { cwd: buildRoot, diff --git a/src/dev/prs/kibana_qa_pr_list.json b/src/dev/prs/kibana_qa_pr_list.json index e8d27ba9f2f0a..503c95d2e7c0f 100644 --- a/src/dev/prs/kibana_qa_pr_list.json +++ b/src/dev/prs/kibana_qa_pr_list.json @@ -89,8 +89,10 @@ "Feature:Observability Landing - Milestone 1", "Feature:Osquery", "Feature:Transforms", +"Feature:Unified Integrations", "Synthetics", "Team: AWL: Platform", +"Team: AWP: Visualization", "Team: Actionable Observability", "Team: CTI", "Team: SecuritySolution", @@ -109,6 +111,7 @@ "Team:Infra Monitoring UI", "Team:Ingest Management", "Team:Observability", +"Team:Unified observability", "Team:Onboarding and Lifecycle Mgt", "Team:Operations", "Team:QA", diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 4167719d3bb31..45b8aad7df8cf 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -8,6 +8,7 @@ // Please also add new aliases to test/scripts/jenkins_storybook.sh export const storybookAliases = { + unified_search: 'src/plugins/unified_search/.storybook', coloring: 'packages/kbn-coloring/.storybook', apm: 'x-pack/plugins/apm/.storybook', canvas: 'x-pack/plugins/canvas/storybook', diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 6d9b372069b22..848ca09a86671 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import glob from 'glob'; +import globby from 'globby'; import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; import { BAZEL_PACKAGE_DIRS } from '@kbn/bazel-packages'; @@ -23,11 +23,8 @@ const createProject = (rootRelativePath: string, options: ProjectOptions = {}) = cache: PROJECT_CACHE, }); -const findProjects = (pattern: string) => - // NOTE: using glob.sync rather than glob-all or globby - // because it takes less than 10 ms, while the other modules - // both took closer to 1000ms. - glob.sync(pattern, { cwd: REPO_ROOT }).map((path) => createProject(path)); +const findProjects = (patterns: string[]) => + globby.sync(patterns, { cwd: REPO_ROOT }).map((path) => createProject(path)); export const PROJECTS = [ createProject('tsconfig.json'), @@ -73,16 +70,18 @@ export const PROJECTS = [ disableTypeCheck: true, }), - ...findProjects('src/plugins/*/tsconfig.json'), - ...findProjects('src/plugins/chart_expressions/*/tsconfig.json'), - ...findProjects('src/plugins/vis_types/*/tsconfig.json'), - ...findProjects('x-pack/plugins/*/tsconfig.json'), - ...findProjects('examples/*/tsconfig.json'), - ...findProjects('x-pack/examples/*/tsconfig.json'), - ...findProjects('test/plugin_functional/plugins/*/tsconfig.json'), - ...findProjects('test/interpreter_functional/plugins/*/tsconfig.json'), - ...findProjects('test/server_integration/__fixtures__/plugins/*/tsconfig.json'), - ...findProjects('packages/kbn-type-summarizer/tests/tsconfig.json'), - - ...BAZEL_PACKAGE_DIRS.flatMap((dir) => findProjects(`${dir}/*/tsconfig.json`)), + // Glob patterns to be all search at once + ...findProjects([ + 'src/plugins/*/tsconfig.json', + 'src/plugins/chart_expressions/*/tsconfig.json', + 'src/plugins/vis_types/*/tsconfig.json', + 'x-pack/plugins/*/tsconfig.json', + 'examples/*/tsconfig.json', + 'x-pack/examples/*/tsconfig.json', + 'test/plugin_functional/plugins/*/tsconfig.json', + 'test/interpreter_functional/plugins/*/tsconfig.json', + 'test/server_integration/__fixtures__/plugins/*/tsconfig.json', + 'packages/kbn-type-summarizer/tests/tsconfig.json', + ...BAZEL_PACKAGE_DIRS.map((dir) => `${dir}/*/tsconfig.json`), + ]), ]; diff --git a/src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt b/src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt index 398a0fdeab61f..517f22bd8ad6a 100644 --- a/src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt +++ b/src/plugins/console/public/application/models/sense_editor/__fixtures__/editor_input1.txt @@ -31,3 +31,7 @@ POST /_sql?format=txt "query": "SELECT prenom FROM claude_index WHERE prenom = 'claude' ", "fetch_size": 1 } + +GET ,,/_search?pretty + +GET kbn:/api/spaces/space \ No newline at end of file diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js index ff9d245f61275..4751d3ca29863 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js @@ -10,6 +10,7 @@ import './sense_editor.test.mocks'; import $ from 'jquery'; import _ from 'lodash'; +import { URL } from 'url'; import { create } from './create'; import { XJson } from '@kbn/es-ui-shared-plugin/public'; @@ -19,6 +20,8 @@ const { collapseLiteralStrings } = XJson; describe('Editor', () => { let input; + let oldUrl; + let olldWindow; beforeEach(function () { // Set up our document body @@ -31,8 +34,19 @@ describe('Editor', () => { input = create(document.querySelector('#ConAppEditor')); $(input.getCoreEditor().getContainer()).show(); input.autocomplete._test.removeChangeListener(); + oldUrl = global.URL; + olldWindow = { ...global.window }; + global.URL = URL; + global.window = Object.create(window); + Object.defineProperty(window, 'location', { + value: { + origin: 'http://localhost:5620', + }, + }); }); afterEach(function () { + global.URL = oldUrl; + global.window = olldWindow; $(input.getCoreEditor().getContainer()).hide(); input.autocomplete._test.addChangeListener(); }); @@ -476,4 +490,20 @@ curl -XPOST "http://localhost:9200/_sql?format=txt" -H "kbn-xsrf: reporting" -H "fetch_size": 1 }'`.trim() ); + + multiReqCopyAsCurlTest( + 'with date math index', + editorInput1, + { start: { lineNumber: 35 }, end: { lineNumber: 35 } }, + ` + curl -XGET "http://localhost:9200/%3Cindex_1-%7Bnow%2Fd-2d%7D%3E%2C%3Cindex_1-%7Bnow%2Fd-1d%7D%3E%2C%3Cindex_1-%7Bnow%2Fd%7D%3E%2F_search?pretty" -H "kbn-xsrf: reporting"`.trim() + ); + + multiReqCopyAsCurlTest( + 'with Kibana API request', + editorInput1, + { start: { lineNumber: 37 }, end: { lineNumber: 37 } }, + ` +curl -XGET "http://localhost:5620/api/spaces/space" -H \"kbn-xsrf: reporting\"`.trim() + ); }); diff --git a/src/plugins/console/public/lib/es/es.ts b/src/plugins/console/public/lib/es/es.ts index 10d0ad95b0496..5e22c78547b96 100644 --- a/src/plugins/console/public/lib/es/es.ts +++ b/src/plugins/console/public/lib/es/es.ts @@ -7,7 +7,7 @@ */ import type { HttpResponse, HttpSetup } from '@kbn/core/public'; -import { trimStart } from 'lodash'; +import { trimStart, trimEnd } from 'lodash'; import { API_BASE_PATH, KIBANA_API_PREFIX } from '../../../common/constants'; const esVersion: string[] = []; @@ -79,11 +79,23 @@ function getKibanaRequestUrl(path: string) { export function constructUrl(baseUri: string, path: string) { const kibanaRequestUrl = getKibanaRequestUrl(path); + let url = `${trimEnd(baseUri, '/')}/${trimStart(path, '/')}`; if (kibanaRequestUrl) { - return kibanaRequestUrl; + url = kibanaRequestUrl; } - baseUri = baseUri.replace(/\/+$/, ''); - path = path.replace(/^\/+/, ''); - return baseUri + '/' + path; + + const { origin, pathname, search } = new URL(url); + return `${origin}${encodePathname(pathname)}${search ?? ''}`; } + +const encodePathname = (path: string) => { + const decodedPath = new URLSearchParams(`path=${path}`).get('path') ?? ''; + + // Skip if it is valid + if (path === decodedPath) { + return path; + } + + return `/${encodeURIComponent(trimStart(decodedPath, '/'))}`; +}; diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 5907c2caff105..7095ad34cd189 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -491,7 +491,7 @@ export function DashboardTopNav({ const showQueryInput = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowQueryInput)); const showDatePicker = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowDatePicker)); const showFilterBar = shouldShowFilterBar(Boolean(embedSettings?.forceHideFilterBar)); - const showQueryBar = showQueryInput || showDatePicker; + const showQueryBar = showQueryInput || showDatePicker || showFilterBar; const showSearchBar = showQueryBar || showFilterBar; const screenTitle = dashboardState.title; diff --git a/src/plugins/data/common/search/search_source/types.ts b/src/plugins/data/common/search/search_source/types.ts index 9ac9c4a057ee9..a3cd83f6ba67a 100644 --- a/src/plugins/data/common/search/search_source/types.ts +++ b/src/plugins/data/common/search/search_source/types.ts @@ -217,3 +217,13 @@ export interface ShardFailure { }; shard: number; } + +export function isSerializedSearchSource( + maybeSerializedSearchSource: unknown +): maybeSerializedSearchSource is SerializedSearchSourceFields { + return ( + typeof maybeSerializedSearchSource === 'object' && + maybeSerializedSearchSource !== null && + !Array.isArray(maybeSerializedSearchSource) + ); +} diff --git a/src/plugins/data/public/search/session/sessions_mgmt/components/index.tsx b/src/plugins/data/public/search/session/sessions_mgmt/components/index.tsx index c9ba988e1330b..05eb03fd60ddf 100644 --- a/src/plugins/data/public/search/session/sessions_mgmt/components/index.tsx +++ b/src/plugins/data/public/search/session/sessions_mgmt/components/index.tsx @@ -15,7 +15,7 @@ export { PopoverActionsMenu } from './actions'; export const TableText = ({ children, ...props }: EuiTextProps) => { return ( - + {children} ); diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 69c2035b28e5c..58b66bb74b5e3 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -108,6 +108,7 @@ export interface IDataPluginServices extends Partial { uiSettings: CoreStart['uiSettings']; savedObjects: CoreStart['savedObjects']; notifications: CoreStart['notifications']; + application: CoreStart['application']; http: CoreStart['http']; storage: IStorageWrapper; data: DataPublicPluginStart; diff --git a/src/plugins/data/server/search/strategies/common/async_utils.test.ts b/src/plugins/data/server/search/strategies/common/async_utils.test.ts new file mode 100644 index 0000000000000..7c90a0fd4c124 --- /dev/null +++ b/src/plugins/data/server/search/strategies/common/async_utils.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 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 { getCommonDefaultAsyncSubmitParams, getCommonDefaultAsyncGetParams } from './async_utils'; +import moment from 'moment'; +import { SearchSessionsConfigSchema } from '../../../../config'; + +const getMockSearchSessionsConfig = ({ + enabled = true, + defaultExpiration = moment.duration(7, 'd'), +} = {}) => + ({ + enabled, + defaultExpiration, + } as SearchSessionsConfigSchema); + +describe('request utils', () => { + describe('getCommonDefaultAsyncSubmitParams', () => { + test('Uses `keep_alive` from default params if no `sessionId` is provided', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + }); + const params = getCommonDefaultAsyncSubmitParams(mockConfig, {}); + expect(params).toHaveProperty('keep_alive', '1m'); + }); + + test('Uses `keep_alive` from config if enabled', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + }); + const params = getCommonDefaultAsyncSubmitParams(mockConfig, { + sessionId: 'foo', + }); + expect(params).toHaveProperty('keep_alive', '259200000ms'); + }); + + test('Uses `keepAlive` of `1m` if disabled', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: false, + }); + const params = getCommonDefaultAsyncSubmitParams(mockConfig, { + sessionId: 'foo', + }); + expect(params).toHaveProperty('keep_alive', '1m'); + }); + + test('Uses `keep_on_completion` if enabled', async () => { + const mockConfig = getMockSearchSessionsConfig({}); + const params = getCommonDefaultAsyncSubmitParams(mockConfig, { + sessionId: 'foo', + }); + expect(params).toHaveProperty('keep_on_completion', true); + }); + + test('Does not use `keep_on_completion` if disabled', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: false, + }); + const params = getCommonDefaultAsyncSubmitParams(mockConfig, { + sessionId: 'foo', + }); + expect(params).toHaveProperty('keep_on_completion', false); + }); + }); + + describe('getCommonDefaultAsyncGetParams', () => { + test('Uses `wait_for_completion_timeout`', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: true, + }); + const params = getCommonDefaultAsyncGetParams(mockConfig, {}); + expect(params).toHaveProperty('wait_for_completion_timeout'); + }); + + test('Uses `keep_alive` if `sessionId` is not provided', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: true, + }); + const params = getCommonDefaultAsyncGetParams(mockConfig, {}); + expect(params).toHaveProperty('keep_alive', '1m'); + }); + + test('Has no `keep_alive` if `sessionId` is provided', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: true, + }); + const params = getCommonDefaultAsyncGetParams(mockConfig, { sessionId: 'foo' }); + expect(params).not.toHaveProperty('keep_alive'); + }); + + test('Uses `keep_alive` if `sessionId` is provided but sessions disabled', async () => { + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: false, + }); + const params = getCommonDefaultAsyncGetParams(mockConfig, { sessionId: 'foo' }); + expect(params).toHaveProperty('keep_alive', '1m'); + }); + }); +}); diff --git a/src/plugins/data/server/search/strategies/common/async_utils.ts b/src/plugins/data/server/search/strategies/common/async_utils.ts new file mode 100644 index 0000000000000..46483ca3f3279 --- /dev/null +++ b/src/plugins/data/server/search/strategies/common/async_utils.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + AsyncSearchSubmitRequest, + AsyncSearchGetRequest, +} from '@elastic/elasticsearch/lib/api/types'; +import { SearchSessionsConfigSchema } from '../../../../config'; +import { ISearchOptions } from '../../../../common'; + +/** + @internal + */ +export function getCommonDefaultAsyncSubmitParams( + searchSessionsConfig: SearchSessionsConfigSchema | null, + options: ISearchOptions +): Pick< + AsyncSearchSubmitRequest, + 'keep_alive' | 'wait_for_completion_timeout' | 'keep_on_completion' +> { + const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; + + const keepAlive = useSearchSessions + ? `${searchSessionsConfig!.defaultExpiration.asMilliseconds()}ms` + : '1m'; + + return { + // Wait up to 100ms for the response to return + wait_for_completion_timeout: '100ms', + // If search sessions are used, store and get an async ID even for short running requests. + keep_on_completion: useSearchSessions, + // The initial keepalive is as defined in defaultExpiration if search sessions are used or 1m otherwise. + keep_alive: keepAlive, + }; +} + +/** + @internal + */ +export function getCommonDefaultAsyncGetParams( + searchSessionsConfig: SearchSessionsConfigSchema | null, + options: ISearchOptions +): Pick { + const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; + + return { + // Wait up to 100ms for the response to return + wait_for_completion_timeout: '100ms', + ...(useSearchSessions + ? // Don't change the expiration of search requests that are tracked in a search session + undefined + : { + // We still need to do polling for searches not within the context of a search session or when search session disabled + keep_alive: '1m', + }), + }; +} diff --git a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts index 33c6f387d6506..13b4295fb7c63 100644 --- a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts @@ -19,7 +19,8 @@ import { toEqlKibanaSearchResponse } from './response_utils'; import { EqlSearchResponse } from './types'; import { ISearchStrategy } from '../../types'; import { getDefaultSearchParams } from '../es_search'; -import { getDefaultAsyncGetParams, getIgnoreThrottled } from '../ese_search/request_utils'; +import { getIgnoreThrottled } from '../ese_search/request_utils'; +import { getCommonDefaultAsyncGetParams } from '../common/async_utils'; export const eqlSearchStrategyProvider = ( logger: Logger @@ -45,11 +46,11 @@ export const eqlSearchStrategyProvider = ( uiSettingsClient ); const params = id - ? getDefaultAsyncGetParams(null, options) + ? getCommonDefaultAsyncGetParams(null, options) : { ...(await getIgnoreThrottled(uiSettingsClient)), ...defaultParams, - ...getDefaultAsyncGetParams(null, options), + ...getCommonDefaultAsyncGetParams(null, options), ...request.params, }; const response = id 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 ea850c80f90b3..07f1c9d1ae9a5 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 @@ -12,6 +12,10 @@ import { AsyncSearchSubmitRequest } from '@elastic/elasticsearch/lib/api/types'; import { ISearchOptions, UI_SETTINGS } from '../../../../common'; import { getDefaultSearchParams } from '../es_search'; import { SearchSessionsConfigSchema } from '../../../../config'; +import { + getCommonDefaultAsyncGetParams, + getCommonDefaultAsyncSubmitParams, +} from '../common/async_utils'; /** * @internal @@ -43,23 +47,10 @@ export async function getDefaultAsyncSubmitParams( | 'keep_on_completion' > > { - const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; - - // TODO: searchSessionsConfig could be "null" if we are running without x-pack which happens only in tests. - // This can be cleaned up when we completely stop separating basic and oss - const keepAlive = useSearchSessions - ? `${searchSessionsConfig!.defaultExpiration.asMilliseconds()}ms` - : '1m'; - return { // TODO: adjust for partial results batched_reduce_size: 64, - // Wait up to 100ms for the response to return - wait_for_completion_timeout: '100ms', - // If search sessions are used, store and get an async ID even for short running requests. - keep_on_completion: useSearchSessions, - // The initial keepalive is as defined in defaultExpiration if search sessions are used or 1m otherwise. - keep_alive: keepAlive, + ...getCommonDefaultAsyncSubmitParams(searchSessionsConfig, options), ...(await getIgnoreThrottled(uiSettingsClient)), ...(await getDefaultSearchParams(uiSettingsClient)), // If search sessions are used, set the initial expiration time. @@ -73,17 +64,7 @@ export function getDefaultAsyncGetParams( searchSessionsConfig: SearchSessionsConfigSchema | null, options: ISearchOptions ): Pick { - const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; - return { - // Wait up to 100ms for the response to return - wait_for_completion_timeout: '100ms', - ...(useSearchSessions - ? // Don't change the expiration of search requests that are tracked in a search session - undefined - : { - // We still need to do polling for searches not within the context of a search session or when search session disabled - keep_alive: '1m', - }), + ...getCommonDefaultAsyncGetParams(searchSessionsConfig, options), }; } diff --git a/src/plugins/data/server/search/strategies/sql_search/request_utils.ts b/src/plugins/data/server/search/strategies/sql_search/request_utils.ts index d05b2710b07ea..de8ced65d16c6 100644 --- a/src/plugins/data/server/search/strategies/sql_search/request_utils.ts +++ b/src/plugins/data/server/search/strategies/sql_search/request_utils.ts @@ -9,6 +9,10 @@ import { SqlGetAsyncRequest, SqlQueryRequest } from '@elastic/elasticsearch/lib/api/types'; import { ISearchOptions } from '../../../../common'; import { SearchSessionsConfigSchema } from '../../../../config'; +import { + getCommonDefaultAsyncGetParams, + getCommonDefaultAsyncSubmitParams, +} from '../common/async_utils'; /** @internal @@ -17,19 +21,8 @@ export function getDefaultAsyncSubmitParams( searchSessionsConfig: SearchSessionsConfigSchema | null, options: ISearchOptions ): Pick { - const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; - - const keepAlive = useSearchSessions - ? `${searchSessionsConfig!.defaultExpiration.asMilliseconds()}ms` - : '1m'; - return { - // Wait up to 100ms for the response to return - wait_for_completion_timeout: '100ms', - // If search sessions are used, store and get an async ID even for short running requests. - keep_on_completion: useSearchSessions, - // The initial keepalive is as defined in defaultExpiration if search sessions are used or 1m otherwise. - keep_alive: keepAlive, + ...getCommonDefaultAsyncSubmitParams(searchSessionsConfig, options), }; } @@ -40,17 +33,7 @@ export function getDefaultAsyncGetParams( searchSessionsConfig: SearchSessionsConfigSchema | null, options: ISearchOptions ): Pick { - const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; - return { - // Wait up to 100ms for the response to return - wait_for_completion_timeout: '100ms', - ...(useSearchSessions - ? // Don't change the expiration of search requests that are tracked in a search session - undefined - : { - // We still need to do polling for searches not within the context of a search session or when search session disabled - keep_alive: '1m', - }), + ...getCommonDefaultAsyncGetParams(searchSessionsConfig, options), }; } diff --git a/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.scss b/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.scss deleted file mode 100644 index ca230711827dc..0000000000000 --- a/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.scss +++ /dev/null @@ -1,5 +0,0 @@ -.testScript__searchBar { - .globalQueryBar { - padding: $euiSize 0 0; - } -} diff --git a/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.tsx b/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.tsx index 52bf882331698..0eb0898f41b60 100644 --- a/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.tsx +++ b/src/plugins/data_view_management/public/components/field_editor/components/scripting_help/test_script.tsx @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import './test_script.scss'; - import React, { Component, Fragment } from 'react'; import { @@ -223,8 +221,11 @@ export class TestScript extends Component { /> + +
{displayIndexPatternEditor} diff --git a/src/plugins/discover/README.md b/src/plugins/discover/README.md index a914d651eef35..6537a830c46a9 100644 --- a/src/plugins/discover/README.md +++ b/src/plugins/discover/README.md @@ -1 +1,32 @@ -Contains the Discover application and the saved search embeddable. \ No newline at end of file +# Discover + +Contains the Discover application and the saved search embeddable. + +## Project tree + +### [src/plugins/discover/public](./public) + +Contains all the client-only code. When you initially load Discover, [public/application/main](./public/application/main) is executed and displayed. + +* **[/application](./public/application)** \ +One folder for every "route", each folder contains files and folders related only to this route. + * **[/context](./public/application/context)** (Also known as "Surrounding documents" - historically this has been a separate plugin) + * **[/doc](./public/application/doc)** (Also known as "Single document" - historically this has been a separate plugin) + * **[/main](./public/application/main)** (Main part of Discover containing the document table) + * **[/not_found](./public/application/not_found)** (Rendered when a route can't be found) + * **[/view_alert](./public/application/view_alert)** (Forwarding links in alert notifications) +* **[/components](./public/components)** (All React components used in more than just one app) +* **[/embeddable](./public/embeddable)** (Code related to the saved search embeddable, rendered on dashboards) +* **[/services](./public/services)** (Services either for external or internal use) +* **[/utils](./public/utils)** (All utility functions used across more than one application) + +### [src/plugins/discover/server](./server) + +Contains all the server-only code. + +### [src/plugins/discover/common](./common)) + +Contains all code shared by client and server. + + + diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 98ce5fc3b0b2b..173264aee731e 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +export const APP_ICON = 'discoverApp'; export const DEFAULT_COLUMNS_SETTING = 'defaultColumns'; export const SAMPLE_SIZE_SETTING = 'discover:sampleSize'; export const SORT_DEFAULT_ORDER_SETTING = 'discover:sort:defaultOrder'; diff --git a/src/plugins/discover/common/services/saved_searches/index.ts b/src/plugins/discover/common/services/saved_searches/index.ts new file mode 100644 index 0000000000000..014fdb31ed438 --- /dev/null +++ b/src/plugins/discover/common/services/saved_searches/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getSavedSearchUrl, getSavedSearchFullPathUrl } from './saved_searches_url'; diff --git a/src/plugins/discover/common/services/saved_searches/saved_searches_url.test.ts b/src/plugins/discover/common/services/saved_searches/saved_searches_url.test.ts new file mode 100644 index 0000000000000..81f4498939b98 --- /dev/null +++ b/src/plugins/discover/common/services/saved_searches/saved_searches_url.test.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 { getSavedSearchUrl, getSavedSearchFullPathUrl } from './saved_searches_url'; + +describe('saved_searches_url', () => { + describe('getSavedSearchUrl', () => { + test('should return valid saved search url', () => { + expect(getSavedSearchUrl()).toBe('#/'); + expect(getSavedSearchUrl('id')).toBe('#/view/id'); + }); + }); + + describe('getSavedSearchFullPathUrl', () => { + test('should return valid full path url', () => { + expect(getSavedSearchFullPathUrl()).toBe('/app/discover#/'); + expect(getSavedSearchFullPathUrl('id')).toBe('/app/discover#/view/id'); + }); + }); +}); diff --git a/src/plugins/discover/common/services/saved_searches/saved_searches_url.ts b/src/plugins/discover/common/services/saved_searches/saved_searches_url.ts new file mode 100644 index 0000000000000..cc5ecdb61f565 --- /dev/null +++ b/src/plugins/discover/common/services/saved_searches/saved_searches_url.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const getSavedSearchUrl = (id?: string) => (id ? `#/view/${encodeURIComponent(id)}` : '#/'); + +export const getSavedSearchFullPathUrl = (id?: string) => `/app/discover${getSavedSearchUrl(id)}`; diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index f9802782f8e48..cb40433b73fa1 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -17,7 +17,7 @@ "dataViewEditor" ], "optionalPlugins": ["home", "share", "usageCollection", "spaces", "triggersActionsUi"], - "requiredBundles": ["kibanaUtils", "kibanaReact", "dataViews"], + "requiredBundles": ["kibanaUtils", "kibanaReact", "dataViews", "unifiedSearch"], "extraPublicDirs": ["common"], "owner": { "name": "Data Discovery", diff --git a/src/plugins/discover/public/application/context/context_app.test.tsx b/src/plugins/discover/public/application/context/context_app.test.tsx index af3c83eb50d6c..828ec0d0eeb1a 100644 --- a/src/plugins/discover/public/application/context/context_app.test.tsx +++ b/src/plugins/discover/public/application/context/context_app.test.tsx @@ -70,7 +70,8 @@ describe('ContextApp test', () => { const topNavProps = { appName: 'context', showSearchBar: true, - showQueryBar: false, + showQueryBar: true, + showQueryInput: false, showFilterBar: true, showSaveQuery: false, showDatePicker: false, diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx index e84bbf644a895..1f886fdacac6b 100644 --- a/src/plugins/discover/public/application/context/context_app.tsx +++ b/src/plugins/discover/public/application/context/context_app.tsx @@ -133,7 +133,8 @@ export const ContextApp = ({ indexPattern, anchorId }: ContextAppProps) => { return { appName: 'context', showSearchBar: true, - showQueryBar: false, + showQueryBar: true, + showQueryInput: false, showFilterBar: true, showSaveQuery: false, showDatePicker: false, diff --git a/src/plugins/discover/public/application/context/services/_stubs.ts b/src/plugins/discover/public/application/context/services/_stubs.ts index df53523c367ae..b37c1ecf4efbf 100644 --- a/src/plugins/discover/public/application/context/services/_stubs.ts +++ b/src/plugins/discover/public/application/context/services/_stubs.ts @@ -8,7 +8,9 @@ import sinon from 'sinon'; import moment from 'moment'; - +import { of } from 'rxjs'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IKibanaSearchResponse } from '@kbn/data-plugin/common'; import { EsHitRecordList } from '../../types'; type SortHit = { @@ -21,6 +23,23 @@ type SortHit = { * A stubbed search source with a `fetch` method that returns all of `_stubHits`. */ export function createSearchSourceStub(hits: EsHitRecordList, timeField?: string) { + const requestResult = { + id: 'Fjk5bndxTHJWU2FldVRVQ0tYR0VqOFEcRWtWNDhOdG5SUzJYcFhONVVZVTBJQToxMDMwOQ==', + rawResponse: { + took: 2, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + hits, + total: hits.length, + }, + }, + isPartial: false, + isRunning: false, + total: 1, + loaded: 1, + isRestored: false, + } as unknown as IKibanaSearchResponse>; // eslint-disable-next-line @typescript-eslint/no-explicit-any const searchSourceStub: any = { _stubHits: hits, @@ -36,14 +55,7 @@ export function createSearchSourceStub(hits: EsHitRecordList, timeField?: string const previousSetCall = searchSourceStub.setField.withArgs(key).lastCall; return previousSetCall ? previousSetCall.args[1] : null; }), - fetch: sinon.spy(() => - Promise.resolve({ - hits: { - hits: searchSourceStub._stubHits, - total: searchSourceStub._stubHits.length, - }, - }) - ), + fetch$: sinon.spy(() => of(requestResult)), }; return searchSourceStub; } @@ -54,7 +66,7 @@ export function createSearchSourceStub(hits: EsHitRecordList, timeField?: string export function createContextSearchSourceStub(timeFieldName: string) { const searchSourceStub = createSearchSourceStub([], timeFieldName); - searchSourceStub.fetch = sinon.spy(() => { + searchSourceStub.fetch$ = sinon.spy(() => { const timeField: keyof SortHit = searchSourceStub._stubTimeField; const lastQuery = searchSourceStub.setField.withArgs('query').lastCall.args[1]; const timeRange = lastQuery.query.bool.must.constant_score.filter.range[timeField]; @@ -72,10 +84,12 @@ export function createContextSearchSourceStub(timeFieldName: string) { ) .sort(sortFunction); - return Promise.resolve({ - hits: { - hits: filteredHits, - total: filteredHits.length, + return of({ + rawResponse: { + hits: { + hits: filteredHits, + total: filteredHits.length, + }, }, }); }); diff --git a/src/plugins/discover/public/application/context/services/anchor.test.ts b/src/plugins/discover/public/application/context/services/anchor.test.ts index 30ff0b039d3e7..ab613fb71cc6d 100644 --- a/src/plugins/discover/public/application/context/services/anchor.test.ts +++ b/src/plugins/discover/public/application/context/services/anchor.test.ts @@ -27,12 +27,12 @@ describe('context app', function () { searchSourceStub = createSearchSourceStub([{ _id: 'hit1' }] as unknown as EsHitRecordList); }); - it('should use the `fetch` method of the SearchSource', function () { + it('should use the `fetch$` method of the SearchSource', function () { return fetchAnchor('id', indexPattern, searchSourceStub, [ { '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }, ]).then(() => { - expect(searchSourceStub.fetch.calledOnce).toBe(true); + expect(searchSourceStub.fetch$.calledOnce).toBe(true); }); }); @@ -142,7 +142,7 @@ describe('context app', function () { }); it('should reject with an error when no hits were found', function () { - searchSourceStub._stubHits = []; + searchSourceStub = createSearchSourceStub([] as unknown as EsHitRecordList); return fetchAnchor('id', indexPattern, searchSourceStub, [ { '@timestamp': SortDirection.desc }, @@ -158,7 +158,10 @@ describe('context app', function () { }); it('should return the first hit after adding an anchor marker', function () { - searchSourceStub._stubHits = [{ property1: 'value1' }, { property2: 'value2' }]; + searchSourceStub = createSearchSourceStub([ + { property1: 'value1' }, + { property2: 'value2' }, + ] as unknown as EsHitRecordList); return fetchAnchor('id', indexPattern, searchSourceStub, [ { '@timestamp': SortDirection.desc }, diff --git a/src/plugins/discover/public/application/context/services/anchor.ts b/src/plugins/discover/public/application/context/services/anchor.ts index a55ed2506537f..28d2298513aa7 100644 --- a/src/plugins/discover/public/application/context/services/anchor.ts +++ b/src/plugins/discover/public/application/context/services/anchor.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 { lastValueFrom } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { ISearchSource, EsQuerySortValue } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; @@ -19,9 +19,8 @@ export async function fetchAnchor( useNewFieldsApi: boolean = false ): Promise { updateSearchSource(searchSource, anchorId, sort, useNewFieldsApi, indexPattern); - - const response = await searchSource.fetch(); - const doc = response.hits?.hits?.[0]; + const { rawResponse } = await lastValueFrom(await searchSource.fetch$()); + const doc = rawResponse.hits?.hits?.[0]; if (!doc) { throw new Error( diff --git a/src/plugins/discover/public/application/context/services/context.predecessors.test.ts b/src/plugins/discover/public/application/context/services/context.predecessors.test.ts index 90fe6bfd7e26b..3b04e7801da4d 100644 --- a/src/plugins/discover/public/application/context/services/context.predecessors.test.ts +++ b/src/plugins/discover/public/application/context/services/context.predecessors.test.ts @@ -89,7 +89,7 @@ describe('context predecessors', function () { return fetchPredecessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 3).then( (hits: EsHitRecordList) => { - expect(mockSearchSource.fetch.calledOnce).toBe(true); + expect(mockSearchSource.fetch$.calledOnce).toBe(true); expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); } ); @@ -234,7 +234,7 @@ describe('context predecessors', function () { (hits: EsHitRecordList) => { const setFieldsSpy = mockSearchSource.setField.withArgs('fields'); const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource'); - expect(mockSearchSource.fetch.calledOnce).toBe(true); + expect(mockSearchSource.fetch$.calledOnce).toBe(true); expect(removeFieldsSpy.calledOnce).toBe(true); expect(setFieldsSpy.calledOnce).toBe(true); expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); diff --git a/src/plugins/discover/public/application/context/services/context.successors.test.ts b/src/plugins/discover/public/application/context/services/context.successors.test.ts index 0c32c3b7ed213..08e5013f1ed80 100644 --- a/src/plugins/discover/public/application/context/services/context.successors.test.ts +++ b/src/plugins/discover/public/application/context/services/context.successors.test.ts @@ -88,7 +88,7 @@ describe('context successors', function () { return fetchSuccessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 3).then( (hits) => { - expect(mockSearchSource.fetch.calledOnce).toBe(true); + expect(mockSearchSource.fetch$.calledOnce).toBe(true); expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); } ); @@ -225,7 +225,7 @@ describe('context successors', function () { return fetchSuccessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 3).then( (hits) => { - expect(mockSearchSource.fetch.calledOnce).toBe(true); + expect(mockSearchSource.fetch$.calledOnce).toBe(true); expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); const setFieldsSpy = mockSearchSource.setField.withArgs('fields'); const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource'); diff --git a/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts b/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts index 1d990f4ae8359..3547127c1ab8c 100644 --- a/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.ts +++ b/src/plugins/discover/public/application/context/utils/fetch_hits_in_interval.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 { lastValueFrom } from 'rxjs'; import { ISearchSource, EsQuerySortValue, SortDirection } from '@kbn/data-plugin/public'; import { convertTimeValueToIso } from './date_conversion'; import { IntervalValue } from './generate_intervals'; @@ -48,7 +48,7 @@ export async function fetchHitsInInterval( if (stop) { range[sortDir === SortDirection.asc ? 'lte' : 'gte'] = convertTimeValueToIso(stop, nanosValue); } - const response = await searchSource + const fetch$ = searchSource .setField('size', maxCount) .setField('query', { query: { @@ -74,8 +74,10 @@ export async function fetchHitsInInterval( .setField('searchAfter', searchAfter) .setField('sort', sort) .setField('version', true) - .fetch(); + .fetch$(); + + const { rawResponse } = await lastValueFrom(fetch$); // TODO: There's a difference in the definition of SearchResponse and EsHitRecord - return (response.hits?.hits as unknown as EsHitRecord[]) || []; + return (rawResponse.hits?.hits as unknown as EsHitRecord[]) || []; } diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss index 1d074c002e340..9ea41f343b885 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss @@ -29,15 +29,14 @@ discover-app { .dscPageBody__contents { overflow: hidden; - padding-top: $euiSizeXS / 2; // A little breathing room for the index pattern button } .dscPageContent__wrapper { - padding: 0 $euiSize $euiSize 0; + padding: $euiSizeS $euiSizeS $euiSizeS 0; overflow: hidden; // Ensures horizontal scroll of table @include euiBreakpoint('xs', 's') { - padding: 0 $euiSize $euiSize; + padding: 0 $euiSizeS $euiSizeS; } } diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index ad1c96e308d12..6cbc8add99c39 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -226,6 +226,9 @@ export function DiscoverLayout({ stateContainer={stateContainer} updateQuery={onUpdateQuery} resetSavedSearch={resetSavedSearch} + onChangeIndexPattern={onChangeIndexPattern} + onEditRuntimeField={onEditRuntimeField} + useNewFieldsApi={useNewFieldsApi} /> - + - - - } - closePopover={[Function]} - data-test-subj="discover-addRuntimeField-popover" - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > -
-
- - - -
-
-
-
- -`; diff --git a/src/plugins/discover/public/application/main/components/sidebar/change_indexpattern.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/change_indexpattern.test.tsx deleted file mode 100644 index a5e93c1d895bc..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/change_indexpattern.test.tsx +++ /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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import React from 'react'; -import { EuiSelectable } from '@elastic/eui'; -import { ShallowWrapper } from 'enzyme'; -import { act } from 'react-dom/test-utils'; -import { shallowWithIntl } from '@kbn/test-jest-helpers'; -import { ChangeIndexPattern } from './change_indexpattern'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; -import { indexPatternWithTimefieldMock } from '../../../../__mocks__/index_pattern_with_timefield'; -import { IndexPatternRef } from './types'; - -function getProps() { - return { - indexPatternId: indexPatternMock.id, - indexPatternRefs: [ - indexPatternMock as IndexPatternRef, - indexPatternWithTimefieldMock as IndexPatternRef, - ], - onChangeIndexPattern: jest.fn(), - trigger: { - label: indexPatternMock.title, - title: indexPatternMock.title, - 'data-test-subj': 'indexPattern-switch-link', - }, - }; -} - -function getIndexPatternPickerList(instance: ShallowWrapper) { - return instance.find(EuiSelectable).first(); -} - -function getIndexPatternPickerOptions(instance: ShallowWrapper) { - return getIndexPatternPickerList(instance).prop('options'); -} - -export function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) { - const options: Array<{ label: string; checked?: 'on' | 'off' }> = getIndexPatternPickerOptions( - instance - ).map((option: { label: string }) => - option.label === selectedLabel - ? { ...option, checked: 'on' } - : { ...option, checked: undefined } - ); - return getIndexPatternPickerList(instance).prop('onChange')!(options); -} - -describe('ChangeIndexPattern', () => { - test('switching index pattern to the same index pattern does not trigger onChangeIndexPattern', async () => { - const props = getProps(); - const comp = shallowWithIntl(); - await act(async () => { - selectIndexPatternPickerOption(comp, indexPatternMock.title); - }); - expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(0); - }); - test('switching index pattern to a different index pattern triggers onChangeIndexPattern', async () => { - const props = getProps(); - const comp = shallowWithIntl(); - await act(async () => { - selectIndexPatternPickerOption(comp, indexPatternWithTimefieldMock.title); - }); - expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(1); - expect(props.onChangeIndexPattern).toHaveBeenCalledWith(indexPatternWithTimefieldMock.id); - }); -}); diff --git a/src/plugins/discover/public/application/main/components/sidebar/change_indexpattern.tsx b/src/plugins/discover/public/application/main/components/sidebar/change_indexpattern.tsx deleted file mode 100644 index ceee905cff6fa..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/change_indexpattern.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import { - EuiButton, - EuiPopover, - EuiPopoverTitle, - EuiSelectable, - EuiButtonProps, -} from '@elastic/eui'; -import { EuiSelectableProps } from '@elastic/eui/src/components/selectable/selectable'; -import { IndexPatternRef } from './types'; - -export type ChangeIndexPatternTriggerProps = EuiButtonProps & { - label: string; - title?: string; -}; - -// TODO: refactor to shared component with ../../../../../../../../x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern - -export function ChangeIndexPattern({ - indexPatternId, - indexPatternRefs, - onChangeIndexPattern, - selectableProps, - trigger, -}: { - indexPatternId?: string; - indexPatternRefs: IndexPatternRef[]; - onChangeIndexPattern: (newId: string) => void; - selectableProps?: EuiSelectableProps<{ value: string }>; - trigger: ChangeIndexPatternTriggerProps; -}) { - const [isPopoverOpen, setPopoverIsOpen] = useState(false); - - const createTrigger = function () { - const { label, title, ...rest } = trigger; - return ( - setPopoverIsOpen(!isPopoverOpen)} - {...rest} - > - {label} - - ); - }; - - return ( - setPopoverIsOpen(false)} - display="block" - panelPaddingSize="s" - > -
- - {i18n.translate('discover.fieldChooser.indexPattern.changeDataViewTitle', { - defaultMessage: 'Change data view', - })} - - - data-test-subj="indexPattern-switcher" - {...selectableProps} - searchable - singleSelection="always" - options={indexPatternRefs.map(({ title, id }) => ({ - label: title, - key: id, - value: id, - checked: id === indexPatternId ? 'on' : undefined, - }))} - onChange={(choices) => { - const choice = choices.find(({ checked }) => checked) as unknown as { - value: string; - }; - if (choice.value !== indexPatternId) { - onChangeIndexPattern(choice.value); - } - setPopoverIsOpen(false); - }} - searchProps={{ - compressed: true, - ...(selectableProps ? selectableProps.searchProps : undefined), - }} - > - {(list, search) => ( - <> - {search} - {list} - - )} - -
-
- ); -} diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.test.tsx deleted file mode 100644 index d640e2fa11594..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.test.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { shallowWithIntl as shallow } from '@kbn/test-jest-helpers'; -import { ShallowWrapper } from 'enzyme'; -import { ChangeIndexPattern } from './change_indexpattern'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObject } from '@kbn/core/server'; -import { DiscoverIndexPattern, DiscoverIndexPatternProps } from './discover_index_pattern'; -import { EuiSelectable } from '@elastic/eui'; -import type { DataView, DataViewAttributes } from '@kbn/data-views-plugin/public'; -import { indexPatternsMock } from '../../../../__mocks__/index_patterns'; - -const indexPattern = { - id: 'the-index-pattern-id-first', - title: 'test1 title', -} as DataView; - -const indexPattern1 = { - id: 'the-index-pattern-id-first', - attributes: { - title: 'test1 title', - }, -} as SavedObject; - -const indexPattern2 = { - id: 'the-index-pattern-id', - attributes: { - title: 'test2 title', - }, -} as SavedObject; - -const defaultProps = { - indexPatternList: [indexPattern1, indexPattern2], - selectedIndexPattern: indexPattern, - useNewFieldsApi: true, - indexPatterns: indexPatternsMock, - onChangeIndexPattern: jest.fn(), -}; - -function getIndexPatternPickerList(instance: ShallowWrapper) { - return instance.find(ChangeIndexPattern).first().dive().find(EuiSelectable); -} - -function getIndexPatternPickerOptions(instance: ShallowWrapper) { - return getIndexPatternPickerList(instance).prop('options'); -} - -function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) { - const options: Array<{ label: string; checked?: 'on' | 'off' }> = getIndexPatternPickerOptions( - instance - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ).map((option: any) => - option.label === selectedLabel - ? { ...option, checked: 'on' } - : { ...option, checked: undefined } - ); - return getIndexPatternPickerList(instance).prop('onChange')!(options); -} - -describe('DiscoverIndexPattern', () => { - test('Invalid props dont cause an exception', () => { - const props = { - indexPatternList: null, - selectedIndexPattern: null, - onChangeIndexPattern: jest.fn(), - } as unknown as DiscoverIndexPatternProps; - - expect(shallow()).toMatchSnapshot(`""`); - }); - test('should list all index patterns', () => { - const instance = shallow(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(getIndexPatternPickerOptions(instance)!.map((option: any) => option.label)).toEqual([ - 'test1 title', - 'test2 title', - ]); - }); - - test('should switch data panel to target index pattern', async () => { - const instance = shallow(); - await act(async () => { - selectIndexPatternPickerOption(instance, 'test2 title'); - }); - expect(defaultProps.onChangeIndexPattern).toHaveBeenCalledWith('the-index-pattern-id'); - }); -}); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.tsx deleted file mode 100644 index 83aa3ce478215..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useState, useEffect } from 'react'; -import { SavedObject } from '@kbn/core/public'; -import type { DataView, DataViewAttributes } from '@kbn/data-views-plugin/public'; -import { IndexPatternRef } from './types'; -import { ChangeIndexPattern } from './change_indexpattern'; - -export interface DiscoverIndexPatternProps { - /** - * list of available index patterns, if length > 1, component offers a "change" link - */ - indexPatternList: Array>; - /** - * Callback function when changing an index pattern - */ - onChangeIndexPattern: (id: string) => void; - /** - * currently selected index pattern - */ - selectedIndexPattern: DataView; -} - -/** - * Component allows you to select an index pattern in discovers side bar - */ -export function DiscoverIndexPattern({ - indexPatternList, - onChangeIndexPattern, - selectedIndexPattern, -}: DiscoverIndexPatternProps) { - const options: IndexPatternRef[] = (indexPatternList || []).map((entity) => ({ - id: entity.id, - title: entity.attributes!.title, - })); - const { id: selectedId, title: selectedTitle } = selectedIndexPattern || {}; - - const [selected, setSelected] = useState({ - id: selectedId, - title: selectedTitle || '', - }); - useEffect(() => { - const { id, title } = selectedIndexPattern; - setSelected({ id, title }); - }, [selectedIndexPattern]); - if (!selectedId) { - return null; - } - - return ( - { - const indexPattern = options.find((pattern) => pattern.id === id); - if (indexPattern) { - onChangeIndexPattern(id); - setSelected(indexPattern); - } - }} - /> - ); -} diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.test.tsx deleted file mode 100644 index cddbe087030e7..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.test.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers'; -import { EuiContextMenuPanel, EuiPopover, EuiContextMenuItem } from '@elastic/eui'; -import { DiscoverServices } from '../../../../build_services'; -import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; -import { stubLogstashIndexPattern } from '@kbn/data-plugin/common/stubs'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; - -const mockServices = { - history: () => ({ - location: { - search: '', - }, - }), - capabilities: { - visualize: { - show: true, - }, - discover: { - save: false, - }, - }, - core: { - application: { - navigateToApp: jest.fn(), - }, - }, - uiSettings: { - get: (key: string) => { - if (key === 'fields:popularLimit') { - return 5; - } - }, - }, - dataViewFieldEditor: { - openEditor: jest.fn(), - userPermissions: { - editIndexPattern: () => { - return true; - }, - }, - }, -} as unknown as DiscoverServices; - -describe('Discover DataView Management', () => { - const indexPattern = stubLogstashIndexPattern; - - const editField = jest.fn(); - const createNewDataView = jest.fn(); - - const mountComponent = () => { - return mountWithIntl( - - - - ); - }; - - test('renders correctly', () => { - const component = mountComponent(); - expect(component).toMatchSnapshot(); - expect(component.find(EuiPopover).length).toBe(1); - }); - - test('click on a button opens popover', () => { - const component = mountComponent(); - expect(component.find(EuiContextMenuPanel).length).toBe(0); - - const button = findTestSubject(component, 'discoverIndexPatternActions'); - button.simulate('click'); - - expect(component.find(EuiContextMenuPanel).length).toBe(1); - expect(component.find(EuiContextMenuItem).length).toBe(3); - }); - - test('click on an add button executes editField callback', () => { - const component = mountComponent(); - const button = findTestSubject(component, 'discoverIndexPatternActions'); - button.simulate('click'); - - const addButton = findTestSubject(component, 'indexPattern-add-field'); - addButton.simulate('click'); - expect(editField).toHaveBeenCalledWith(undefined); - }); - - test('click on a manage button navigates away from discover', () => { - const component = mountComponent(); - const button = findTestSubject(component, 'discoverIndexPatternActions'); - button.simulate('click'); - - const manageButton = findTestSubject(component, 'indexPattern-manage-field'); - manageButton.simulate('click'); - expect(mockServices.core.application.navigateToApp).toHaveBeenCalled(); - }); - - test('click on add dataView button executes createNewDataView callback', () => { - const component = mountComponent(); - const button = findTestSubject(component, 'discoverIndexPatternActions'); - button.simulate('click'); - - const manageButton = findTestSubject(component, 'dataview-create-new'); - manageButton.simulate('click'); - expect(createNewDataView).toHaveBeenCalled(); - }); -}); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx deleted file mode 100644 index 823aa9c0050c0..0000000000000 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useState } from 'react'; -import { - EuiButtonIcon, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiHorizontalRule, - EuiPopover, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; - -export interface DiscoverIndexPatternManagementProps { - /** - * Currently selected index pattern - */ - selectedIndexPattern?: DataView; - /** - * Read from the Fields API - */ - useNewFieldsApi?: boolean; - /** - * Callback to execute on edit field action - * @param fieldName - */ - editField: (fieldName?: string) => void; - - /** - * Callback to execute on create new data action - */ - createNewDataView: () => void; -} - -export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManagementProps) { - const { dataViewFieldEditor, core } = useDiscoverServices(); - const { useNewFieldsApi, selectedIndexPattern, editField, createNewDataView } = props; - const dataViewEditPermission = dataViewFieldEditor?.userPermissions.editIndexPattern(); - const canEditDataViewField = !!dataViewEditPermission && useNewFieldsApi; - const [isAddIndexPatternFieldPopoverOpen, setIsAddIndexPatternFieldPopoverOpen] = useState(false); - - if (!useNewFieldsApi || !selectedIndexPattern || !canEditDataViewField) { - return null; - } - - const addField = () => { - editField(undefined); - }; - - return ( - { - setIsAddIndexPatternFieldPopoverOpen(false); - }} - ownFocus - data-test-subj="discover-addRuntimeField-popover" - button={ - { - setIsAddIndexPatternFieldPopoverOpen(!isAddIndexPatternFieldPopoverOpen); - }} - /> - } - > - { - setIsAddIndexPatternFieldPopoverOpen(false); - addField(); - }} - > - {i18n.translate('discover.fieldChooser.indexPatterns.addFieldButton', { - defaultMessage: 'Add field', - })} - , - { - setIsAddIndexPatternFieldPopoverOpen(false); - core.application.navigateToApp('management', { - path: `/kibana/indexPatterns/patterns/${props.selectedIndexPattern?.id}`, - }); - }} - > - {i18n.translate('discover.fieldChooser.indexPatterns.manageFieldButton', { - defaultMessage: 'Manage settings', - })} - , - , - { - setIsAddIndexPatternFieldPopoverOpen(false); - createNewDataView(); - }} - > - {i18n.translate('discover.fieldChooser.dataViews.createNewDataView', { - defaultMessage: 'Create new data view', - })} - , - ]} - /> - - ); -} diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss index 9ef123fa1a60f..6845b1c89901d 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.scss @@ -2,7 +2,7 @@ overflow: hidden; margin: 0 !important; flex-grow: 1; - padding-left: $euiSize; + padding: $euiSizeS 0 $euiSizeS $euiSizeS; width: $euiSize * 19; height: 100%; @@ -19,7 +19,7 @@ .dscSidebar__mobile { width: 100%; - padding: $euiSize $euiSize 0; + padding: $euiSizeS $euiSizeS 0; .dscSidebar__mobileBadge { margin-left: $euiSizeS; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx index fb6af1bc1b775..22f954e714987 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx @@ -20,16 +20,17 @@ import { EuiNotificationBadge, EuiPageSideBar, useResizeObserver, + EuiButton, } from '@elastic/eui'; import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect'; -import { isEqual, sortBy } from 'lodash'; +import { isEqual } from 'lodash'; import { FormattedMessage } from '@kbn/i18n-react'; import { indexPatterns as indexPatternUtils } from '@kbn/data-plugin/public'; +import { DataViewPicker } from '@kbn/unified-search-plugin/public'; import { DataViewField } from '@kbn/data-views-plugin/public'; import { useDiscoverServices } from '../../../../utils/use_discover_services'; import { DiscoverField } from './discover_field'; -import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; import { FIELDS_LIMIT_SETTING } from '../../../../../common'; import { groupFields } from './lib/group_fields'; @@ -37,7 +38,6 @@ import { getDetails } from './lib/get_details'; import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive'; -import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { ElasticSearchHit } from '../../../../types'; @@ -83,6 +83,8 @@ export interface DiscoverSidebarProps extends Omit(null); @@ -297,34 +299,6 @@ export function DiscoverSidebarComponent({ return null; } - if (useFlyout) { - return ( -
- - - o.attributes.title)} - onChangeIndexPattern={onChangeIndexPattern} - /> - - - - - -
- ); - } - return ( - - - - o.attributes.title)} - onChangeIndexPattern={onChangeIndexPattern} - /> - - - - - - + {Boolean(showDataViewPicker) && ( + + )}
+ + editField()} + size="s" + > + {i18n.translate('discover.fieldChooser.addField.label', { + defaultMessage: 'Add a field', + })} + + ); diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index f2f58c43d5e7f..f7664197ca98c 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -7,7 +7,6 @@ */ import React, { useEffect, useRef, useState, useCallback } from 'react'; -import { sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { UiCounterMetricType } from '@kbn/analytics'; @@ -19,21 +18,16 @@ import { EuiBadge, EuiFlyoutHeader, EuiFlyout, - EuiSpacer, EuiIcon, EuiLink, EuiPortal, - EuiFlexGroup, - EuiFlexItem, } from '@elastic/eui'; import type { DataViewField, DataView, DataViewAttributes } from '@kbn/data-views-plugin/public'; import { SavedObject } from '@kbn/core/types'; import { useDiscoverServices } from '../../../../utils/use_discover_services'; -import { DiscoverIndexPattern } from './discover_index_pattern'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; import { AppState } from '../../services/discover_state'; -import { DiscoverIndexPatternManagement } from './discover_index_pattern_management'; import { AvailableFields$, DataDocuments$ } from '../../utils/use_saved_search'; import { calcFieldCounts } from '../../utils/calc_field_counts'; import { VIEW_MODE } from '../../../../components/view_mode_toggle'; @@ -91,10 +85,6 @@ export interface DiscoverSidebarResponsiveProps { * @param eventName */ trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; - /** - * Shows index pattern and a button that displays the sidebar in a flyout - */ - useFlyout?: boolean; /** * Read from the Fields API */ @@ -124,13 +114,7 @@ export interface DiscoverSidebarResponsiveProps { */ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) { const services = useDiscoverServices(); - const { - selectedIndexPattern, - onEditRuntimeField, - useNewFieldsApi, - onChangeIndexPattern, - onDataViewCreated, - } = props; + const { selectedIndexPattern, onEditRuntimeField, useNewFieldsApi, onDataViewCreated } = props; const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter()); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); /** @@ -291,34 +275,6 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) )}
-
- - - o.attributes.title)} - /> - - - - - -
- -
diff --git a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx index 938d2d55df004..7b8831f734279 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx @@ -40,6 +40,8 @@ function getProps(savePermissions = true): DiscoverTopNavProps { onOpenInspector: jest.fn(), searchSource: {} as ISearchSource, resetSavedSearch: () => {}, + onEditRuntimeField: jest.fn(), + onChangeIndexPattern: jest.fn(), }; } diff --git a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx index 8656a2fdb7072..87d2f04bd604b 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useRef, useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { Query, TimeRange } from '@kbn/data-plugin/public'; import { DataViewType } from '@kbn/data-views-plugin/public'; @@ -25,6 +25,9 @@ export type DiscoverTopNavProps = Pick< updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; stateContainer: GetStateReturn; resetSavedSearch: () => void; + onChangeIndexPattern: (indexPattern: string) => void; + onEditRuntimeField: () => void; + useNewFieldsApi?: boolean; }; export const DiscoverTopNav = ({ @@ -38,6 +41,9 @@ export const DiscoverTopNav = ({ navigateTo, savedSearch, resetSavedSearch, + onChangeIndexPattern, + onEditRuntimeField, + useNewFieldsApi = false, }: DiscoverTopNavProps) => { const history = useHistory(); const showDatePicker = useMemo( @@ -45,7 +51,16 @@ export const DiscoverTopNav = ({ [indexPattern] ); const services = useDiscoverServices(); - const { TopNavMenu } = services.navigation.ui; + const { dataViewEditor, navigation, dataViewFieldEditor, data } = services; + const editPermission = useMemo( + () => dataViewFieldEditor.userPermissions.editIndexPattern(), + [dataViewFieldEditor] + ); + const canEditDataViewField = !!editPermission && useNewFieldsApi; + const closeFieldEditor = useRef<() => void | undefined>(); + const closeDataViewEditor = useRef<() => void | undefined>(); + + const { TopNavMenu } = navigation.ui; const onOpenSavedSearch = useCallback( (newSavedSearchId: string) => { @@ -58,6 +73,64 @@ export const DiscoverTopNav = ({ [history, resetSavedSearch, savedSearch.id] ); + useEffect(() => { + return () => { + // Make sure to close the editors when unmounting + if (closeFieldEditor.current) { + closeFieldEditor.current(); + } + if (closeDataViewEditor.current) { + closeDataViewEditor.current(); + } + }; + }, []); + + const editField = useMemo( + () => + canEditDataViewField + ? async (fieldName?: string, uiAction: 'edit' | 'add' = 'edit') => { + if (indexPattern?.id) { + const indexPatternInstance = await data.dataViews.get(indexPattern.id); + closeFieldEditor.current = dataViewFieldEditor.openEditor({ + ctx: { + dataView: indexPatternInstance, + }, + fieldName, + onSave: async () => { + onEditRuntimeField(); + }, + }); + } + } + : undefined, + [ + canEditDataViewField, + indexPattern?.id, + data.dataViews, + dataViewFieldEditor, + onEditRuntimeField, + ] + ); + + const addField = useMemo( + () => (canEditDataViewField && editField ? () => editField(undefined, 'add') : undefined), + [editField, canEditDataViewField] + ); + + const createNewDataView = useCallback(() => { + const indexPatternFieldEditPermission = dataViewEditor.userPermissions.editDataView; + if (!indexPatternFieldEditPermission) { + return; + } + closeDataViewEditor.current = dataViewEditor.openEditor({ + onSave: async (dataView) => { + if (dataView.id) { + onChangeIndexPattern(dataView.id); + } + }, + }); + }, [dataViewEditor, onChangeIndexPattern]); + const topNavMenu = useMemo( () => getTopNavLinks({ @@ -99,6 +172,18 @@ export const DiscoverTopNav = ({ return getHeaderActionMenuMounter(); }, []); + const dataViewPickerProps = { + trigger: { + label: indexPattern?.title || '', + 'data-test-subj': 'discover-dataView-switch-link', + title: indexPattern?.title || '', + }, + currentDataViewId: indexPattern?.id, + onAddField: addField, + onDataViewCreated: createNewDataView, + onChangeDataView: (newIndexPatternId: string) => onChangeIndexPattern(newIndexPatternId), + }; + return ( ); }; diff --git a/src/plugins/discover/public/application/main/discover_main_app.test.tsx b/src/plugins/discover/public/application/main/discover_main_app.test.tsx index ceb06df058fae..d2f0c7e2dd005 100644 --- a/src/plugins/discover/public/application/main/discover_main_app.test.tsx +++ b/src/plugins/discover/public/application/main/discover_main_app.test.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { indexPatternMock } from '../../__mocks__/index_pattern'; import { DiscoverMainApp } from './discover_main_app'; +import { DiscoverTopNav } from './components/top_nav/discover_topnav'; import { savedSearchMock } from '../../__mocks__/saved_search'; import { SavedObject } from '@kbn/core/types'; import type { DataViewAttributes } from '@kbn/data-views-plugin/public'; import { setHeaderActionMenuMounter } from '../../kibana_services'; -import { findTestSubject } from '@elastic/eui/lib/test'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { discoverServiceMock } from '../../__mocks__/services'; import { Router } from 'react-router-dom'; @@ -42,8 +42,7 @@ describe('DiscoverMainApp', () => { ); - expect(findTestSubject(component, 'indexPattern-switch-link').text()).toBe( - indexPatternMock.title - ); + expect(component.find(DiscoverTopNav).exists()).toBe(true); + expect(component.find(DiscoverTopNav).prop('indexPattern')).toEqual(indexPatternMock); }); }); diff --git a/src/plugins/discover/public/components/discover_grid/constants.ts b/src/plugins/discover/public/components/discover_grid/constants.ts index d026607aef373..f2f5a8e8bebc7 100644 --- a/src/plugins/discover/public/components/discover_grid/constants.ts +++ b/src/plugins/discover/public/components/discover_grid/constants.ts @@ -19,7 +19,7 @@ export const GRID_STYLE = { export const pageSizeArr = [25, 50, 100, 250]; export const defaultPageSize = 100; -export const defaultTimeColumnWidth = 190; +export const defaultTimeColumnWidth = 210; export const toolbarVisibility = { showColumnSelector: { allowHide: false, diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.scss b/src/plugins/discover/public/components/discover_grid/discover_grid.scss index 0204433a5ba1c..113bb60924850 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid.scss +++ b/src/plugins/discover/public/components/discover_grid/discover_grid.scss @@ -30,6 +30,15 @@ } } +.dscDiscoverGrid__cellValue { + font-family: $euiCodeFontFamily; +} + +.dscDiscoverGrid__cellPopoverValue { + font-family: $euiCodeFontFamily; + font-size: $euiFontSizeS; +} + .dscDiscoverGrid__footer { background-color: $euiColorLightShade; padding: $euiSize / 2 $euiSize; diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx index a9116e616946f..c98db31a97f7f 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx @@ -207,7 +207,7 @@ describe('Discover grid columns', function () { /> , "id": "timestamp", - "initialWidth": 190, + "initialWidth": 210, "isSortable": true, "schema": "datetime", }, diff --git a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx index be4c69f1ced25..53e5c23cb47d5 100644 --- a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx @@ -92,7 +92,9 @@ describe('Discover grid cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component.html()).toMatchInlineSnapshot(`"100"`); + expect(component.html()).toMatchInlineSnapshot( + `"100"` + ); }); it('renders bytes column correctly using _source when details is true', () => { @@ -115,7 +117,9 @@ describe('Discover grid cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component.html()).toMatchInlineSnapshot(`"100"`); + expect(component.html()).toMatchInlineSnapshot( + `"100"` + ); }); it('renders bytes column correctly using fields when details is true', () => { @@ -138,7 +142,9 @@ describe('Discover grid cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component.html()).toMatchInlineSnapshot(`"100"`); + expect(component.html()).toMatchInlineSnapshot( + `"100"` + ); }); it('renders _source column correctly', () => { @@ -163,7 +169,7 @@ describe('Discover grid cell rendering', function () { ); expect(component).toMatchInlineSnapshot(` @@ -280,7 +286,7 @@ describe('Discover grid cell rendering', function () { ); expect(component).toMatchInlineSnapshot(` @@ -359,7 +365,7 @@ describe('Discover grid cell rendering', function () { ); expect(component).toMatchInlineSnapshot(` @@ -485,7 +491,7 @@ describe('Discover grid cell rendering', function () { ); expect(component).toMatchInlineSnapshot(` @@ -527,7 +533,7 @@ describe('Discover grid cell rendering', function () { ); expect(component).toMatchInlineSnapshot(` @@ -603,6 +609,7 @@ describe('Discover grid cell rendering', function () { ); expect(component).toMatchInlineSnapshot(` ); - expect(component.html()).toMatchInlineSnapshot(`"-"`); + expect(component.html()).toMatchInlineSnapshot( + `"-"` + ); }); it('renders correctly when invalid column is given', () => { @@ -657,7 +666,9 @@ describe('Discover grid cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component.html()).toMatchInlineSnapshot(`"-"`); + expect(component.html()).toMatchInlineSnapshot( + `"-"` + ); }); it('renders unmapped fields correctly', () => { @@ -695,6 +706,7 @@ describe('Discover grid cell rendering', function () { ); expect(component).toMatchInlineSnapshot(` -; + return -; } /** @@ -102,7 +105,11 @@ export const getRenderCellValueFn = : formatHit(row, dataView, fieldsToShow, maxEntries, fieldFormats); return ( - + {pairs.map(([key, value]) => ( {key} @@ -118,6 +125,7 @@ export const getRenderCellValueFn = return ( { - describe('getSavedSearchUrl', () => { - test('should return valid saved search url', () => { - expect(getSavedSearchUrl()).toBe('#/'); - expect(getSavedSearchUrl('id')).toBe('#/view/id'); - }); - }); - - describe('getSavedSearchFullPathUrl', () => { - test('should return valid full path url', () => { - expect(getSavedSearchFullPathUrl()).toBe('/app/discover#/'); - expect(getSavedSearchFullPathUrl('id')).toBe('/app/discover#/view/id'); - }); - }); - describe('fromSavedSearchAttributes', () => { test('should convert attributes into SavedSearch', () => { const attributes: SavedSearchAttributes = { diff --git a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts index 4dbb84613ead8..26b3c0b7cf9b5 100644 --- a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts +++ b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts @@ -8,9 +8,10 @@ import { i18n } from '@kbn/i18n'; import type { SavedSearchAttributes, SavedSearch } from './types'; -export const getSavedSearchUrl = (id?: string) => (id ? `#/view/${encodeURIComponent(id)}` : '#/'); - -export const getSavedSearchFullPathUrl = (id?: string) => `/app/discover${getSavedSearchUrl(id)}`; +export { + getSavedSearchUrl, + getSavedSearchFullPathUrl, +} from '../../../common/services/saved_searches'; export const getSavedSearchUrlConflictMessage = async (savedSearch: SavedSearch) => i18n.translate('discover.savedSearchEmbeddable.legacyURLConflict.errorMessage', { diff --git a/src/plugins/discover/server/plugin.ts b/src/plugins/discover/server/plugin.ts index 9147f533d28d6..888fcf55c2351 100644 --- a/src/plugins/discover/server/plugin.ts +++ b/src/plugins/discover/server/plugin.ts @@ -8,15 +8,18 @@ import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server'; import type { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server'; +import type { HomeServerPluginSetup } from '@kbn/home-plugin/server'; import { getUiSettings } from './ui_settings'; import { capabilitiesProvider } from './capabilities_provider'; import { getSavedSearchObjectType } from './saved_objects'; +import { registerSampleData } from './sample_data'; export class DiscoverServerPlugin implements Plugin { public setup( core: CoreSetup, plugins: { data: DataPluginSetup; + home?: HomeServerPluginSetup; } ) { const getSearchSourceMigrations = plugins.data.search.searchSource.getAllMigrations.bind( @@ -26,6 +29,10 @@ export class DiscoverServerPlugin implements Plugin { core.uiSettings.register(getUiSettings(core.docLinks)); core.savedObjects.registerType(getSavedSearchObjectType(getSearchSourceMigrations)); + if (plugins.home) { + registerSampleData(plugins.home.sampleData); + } + return {}; } diff --git a/src/plugins/discover/server/sample_data/index.ts b/src/plugins/discover/server/sample_data/index.ts new file mode 100644 index 0000000000000..43edd42293edf --- /dev/null +++ b/src/plugins/discover/server/sample_data/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { registerSampleData } from './register_sample_data'; diff --git a/src/plugins/discover/server/sample_data/register_sample_data.ts b/src/plugins/discover/server/sample_data/register_sample_data.ts new file mode 100644 index 0000000000000..a1ff9951d9179 --- /dev/null +++ b/src/plugins/discover/server/sample_data/register_sample_data.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { SampleDataRegistrySetup } from '@kbn/home-plugin/server'; +import { APP_ICON } from '../../common'; +import { getSavedSearchFullPathUrl } from '../../common/services/saved_searches'; + +function getDiscoverPathForSampleDataset(objId: string) { + // TODO: remove the time range from the URL query when saved search objects start supporting time range configuration + // https://github.com/elastic/kibana/issues/9761 + return `${getSavedSearchFullPathUrl(objId)}?_g=(time:(from:now-7d,to:now))`; +} + +export function registerSampleData(sampleDataRegistry: SampleDataRegistrySetup) { + const linkLabel = i18n.translate('discover.sampleData.viewLinkLabel', { + defaultMessage: 'Discover', + }); + const { addAppLinksToSampleDataset, getSampleDatasets } = sampleDataRegistry; + const sampleDatasets = getSampleDatasets(); + + sampleDatasets.forEach((sampleDataset) => { + const sampleSavedSearchObject = sampleDataset.savedObjects.find( + (object) => object.type === 'search' + ); + + if (sampleSavedSearchObject) { + addAppLinksToSampleDataset(sampleDataset.id, [ + { + sampleObject: sampleSavedSearchObject, + getPath: getDiscoverPathForSampleDataset, + label: linkLabel, + icon: APP_ICON, + order: -1, + }, + ]); + } + }); +} diff --git a/src/plugins/discover/server/saved_objects/search_migrations.test.ts b/src/plugins/discover/server/saved_objects/search_migrations.test.ts index 9563bd6dc86c3..fcce5d41fe90b 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.test.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.test.ts @@ -350,6 +350,7 @@ Object { testMigrateMatchAllQuery(migrationFn); }); }); + it('should apply search source migrations within saved search', () => { const savedSearch = { attributes: { @@ -379,4 +380,27 @@ Object { }, }); }); + + it('should not apply search source migrations within saved search when searchSourceJSON is not an object', () => { + const savedSearch = { + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '5', + }, + }, + } as SavedObjectUnsanitizedDoc; + + const versionToTest = '9.1.2'; + const migrations = getAllMigrations({ + [versionToTest]: (state) => ({ ...state, migrated: true }), + }); + + expect(migrations[versionToTest](savedSearch, {} as SavedObjectMigrationContext)).toEqual({ + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '5', + }, + }, + }); + }); }); diff --git a/src/plugins/discover/server/saved_objects/search_migrations.ts b/src/plugins/discover/server/saved_objects/search_migrations.ts index 95da82fa38acf..2fb49628f53bc 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.ts @@ -17,7 +17,7 @@ import type { import { mergeSavedObjectMigrationMaps } from '@kbn/core/server'; import { DEFAULT_QUERY_LANGUAGE } from '@kbn/data-plugin/server'; import { MigrateFunctionsObject, MigrateFunction } from '@kbn/kibana-utils-plugin/common'; -import type { SerializedSearchSourceFields } from '@kbn/data-plugin/common'; +import { isSerializedSearchSource, SerializedSearchSourceFields } from '@kbn/data-plugin/common'; export interface SavedSearchMigrationAttributes extends SavedObjectAttributes { kibanaSavedObjectMeta: { @@ -135,27 +135,31 @@ const migrateSearchSortToNestedArray: SavedObjectMigrationFn = (doc) = /** * This creates a migration map that applies search source migrations */ -const getSearchSourceMigrations = (searchSourceMigrations: MigrateFunctionsObject) => +const getSearchSourceMigrations = ( + searchSourceMigrations: MigrateFunctionsObject +): MigrateFunctionsObject => mapValues( searchSourceMigrations, (migrate: MigrateFunction): MigrateFunction => (state) => { - const _state = state as unknown as { attributes: SavedSearchMigrationAttributes }; - - const parsedSearchSourceJSON = _state.attributes.kibanaSavedObjectMeta.searchSourceJSON; - - if (!parsedSearchSourceJSON) return _state; - - return { - ..._state, - attributes: { - ..._state.attributes, - kibanaSavedObjectMeta: { - ..._state.attributes.kibanaSavedObjectMeta, - searchSourceJSON: JSON.stringify(migrate(JSON.parse(parsedSearchSourceJSON))), + const _state = state as { attributes: SavedSearchMigrationAttributes }; + + const parsedSearchSourceJSON = JSON.parse( + _state.attributes.kibanaSavedObjectMeta.searchSourceJSON + ); + if (isSerializedSearchSource(parsedSearchSourceJSON)) { + return { + ..._state, + attributes: { + ..._state.attributes, + kibanaSavedObjectMeta: { + ..._state.attributes.kibanaSavedObjectMeta, + searchSourceJSON: JSON.stringify(migrate(parsedSearchSourceJSON)), + }, }, - }, - }; + }; + } + return _state; } ); @@ -171,6 +175,6 @@ export const getAllMigrations = ( ): SavedObjectMigrationMap => { return mergeSavedObjectMigrationMaps( searchMigrations, - getSearchSourceMigrations(searchSourceMigrations) as unknown as SavedObjectMigrationMap + getSearchSourceMigrations(searchSourceMigrations) as SavedObjectMigrationMap ); }; diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 817e73f16617e..9915680ada26e 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -25,6 +25,7 @@ { "path": "../data_view_field_editor/tsconfig.json"}, { "path": "../field_formats/tsconfig.json" }, { "path": "../data_views/tsconfig.json" }, + { "path": "../unified_search/tsconfig.json" }, { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, { "path": "../data_view_editor/tsconfig.json" }, { "path": "../../../x-pack/plugins/triggers_actions_ui/tsconfig.json" } diff --git a/src/plugins/home/public/application/components/__snapshots__/sample_data_view_data_button.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/sample_data_view_data_button.test.js.snap index d9e341394ee00..0d634049305ad 100644 --- a/src/plugins/home/public/application/components/__snapshots__/sample_data_view_data_button.test.js.snap +++ b/src/plugins/home/public/application/components/__snapshots__/sample_data_view_data_button.test.js.snap @@ -57,6 +57,90 @@ exports[`should render popover when appLinks is not empty 1`] = ` `; +exports[`should render popover with ordered appLinks 1`] = ` + + View data + + } + closePopover={[Function]} + data-test-subj="launchSampleDataSetecommerce" + display="inlineBlock" + hasArrow={true} + id="sampleDataLinksecommerce" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" +> + , + "name": "myAppLabel[-1]", + "onClick": [Function], + }, + Object { + "data-test-subj": "viewSampleDataSetecommerce-dashboard", + "href": "root/app/dashboards#/view/722b74f0-b882-11e8-a6d9-e546fe2bba5f", + "icon": , + "name": "Dashboard", + "onClick": [Function], + }, + Object { + "href": "rootapp/myAppPath", + "icon": , + "name": "myAppLabel[3]", + "onClick": [Function], + }, + Object { + "href": "rootapp/myAppPath", + "icon": , + "name": "myAppLabel[5]", + "onClick": [Function], + }, + Object { + "href": "rootapp/myAppPath", + "icon": , + "name": "myAppLabel", + "onClick": [Function], + }, + ], + }, + ] + } + size="m" + /> + +`; + exports[`should render simple button when appLinks is empty 1`] = ` { + const dashboardAppLink = { + path: dashboardPath, + label: i18n.translate('home.sampleDataSetCard.dashboardLinkLabel', { + defaultMessage: 'Dashboard', + }), + icon: 'dashboardApp', + order: 0, + 'data-test-subj': `viewSampleDataSet${this.props.id}-dashboard`, + }; + + const sortedItems = sortBy([dashboardAppLink, ...this.props.appLinks], 'order'); + const items = sortedItems.map(({ path, label, icon, ...rest }) => { return { name: label, icon: , href: this.addBasePath(path), onClick: createAppNavigationHandler(path), + ...(rest['data-test-subj'] ? { 'data-test-subj': rest['data-test-subj'] } : {}), }; }); @@ -75,18 +87,7 @@ export class SampleDataViewDataButton extends React.Component { const panels = [ { id: 0, - items: [ - { - name: i18n.translate('home.sampleDataSetCard.dashboardLinkLabel', { - defaultMessage: 'Dashboard', - }), - icon: , - href: prefixedDashboardPath, - onClick: createAppNavigationHandler(dashboardPath), - 'data-test-subj': `viewSampleDataSet${this.props.id}-dashboard`, - }, - ...additionalItems, - ], + items, }, ]; const popoverButton = ( @@ -124,6 +125,7 @@ SampleDataViewDataButton.propTypes = { path: PropTypes.string.isRequired, label: PropTypes.string.isRequired, icon: PropTypes.string.isRequired, + order: PropTypes.number, }) ).isRequired, }; diff --git a/src/plugins/home/public/application/components/sample_data_view_data_button.test.js b/src/plugins/home/public/application/components/sample_data_view_data_button.test.js index b097b5e322500..f3cfd5a7a661e 100644 --- a/src/plugins/home/public/application/components/sample_data_view_data_button.test.js +++ b/src/plugins/home/public/application/components/sample_data_view_data_button.test.js @@ -48,3 +48,41 @@ test('should render popover when appLinks is not empty', () => { ); expect(component).toMatchSnapshot(); // eslint-disable-line }); + +test('should render popover with ordered appLinks', () => { + const appLinks = [ + { + path: 'app/myAppPath', + label: 'myAppLabel[-1]', + icon: 'logoKibana', + order: -1, // to position it above Dashboard link + }, + { + path: 'app/myAppPath', + label: 'myAppLabel', + icon: 'logoKibana', + }, + { + path: 'app/myAppPath', + label: 'myAppLabel[5]', + icon: 'logoKibana', + order: 5, + }, + { + path: 'app/myAppPath', + label: 'myAppLabel[3]', + icon: 'logoKibana', + order: 3, + }, + ]; + + const component = shallow( + + ); + expect(component).toMatchSnapshot(); // eslint-disable-line +}); diff --git a/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts b/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts index 8d26d08460b5b..9b1212e13b024 100644 --- a/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts +++ b/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts @@ -58,4 +58,11 @@ export interface AppLinkData { * The icon for this app link. */ icon: string; + /** + * Index of the links (ascending order, smallest will be displayed first). + * Used for ordering in the dropdown. + * + * @remark links without order defined will be displayed last + */ + order?: number; } diff --git a/src/plugins/home/server/services/sample_data/routes/list.ts b/src/plugins/home/server/services/sample_data/routes/list.ts index 39690b3944d0c..a83ee7a57c432 100644 --- a/src/plugins/home/server/services/sample_data/routes/list.ts +++ b/src/plugins/home/server/services/sample_data/routes/list.ts @@ -35,12 +35,12 @@ export const createListRoute = ( ?.foundObjectId ?? id; const appLinks = (appLinksMap.get(sampleDataset.id) ?? []).map((data) => { - const { sampleObject, getPath, label, icon } = data; + const { sampleObject, getPath, label, icon, order } = data; if (sampleObject === null) { - return { path: getPath(''), label, icon }; + return { path: getPath(''), label, icon, order }; } const objectId = findObjectId(sampleObject.type, sampleObject.id); - return { path: getPath(objectId), label, icon }; + return { path: getPath(objectId), label, icon, order }; }); const sampleDataStatus = await getSampleDatasetStatus( context, diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md b/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md index 1f7344a801227..9b2c6690626fd 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md @@ -120,10 +120,9 @@ This collection occurs by default for every application registered via the menti In order to keep the count of the events, this collector uses 3 Saved Objects: -1. `application_usage_transactional`: It stores each individually reported event. Grouped by `timestamp` and `appId`. The reason for having these documents instead of editing `application_usage_daily` documents on very report is to provide faster response to the requests to `/api/ui_counters/_report` (creating new documents instead of finding and editing existing ones) and to avoid conflicts when multiple users reach to the API concurrently. -2. `application_usage_daily`: Periodically, documents from `application_usage_transactional` are aggregated to daily summaries and deleted. Also grouped by `timestamp` and `appId` for the main view concatenated with `viewId` for other views. -3. `application_usage_totals`: It stores the sum of all the events older than 90 days old, grouped by `appId` for the main view concatenated with `viewId` for other views. +1. `application_usage_transactional`: It stores each individually reported event. Grouped by `timestamp` and `appId`. +2. `application_usage_totals`: It stores the sum of all the events older than 90 days old, grouped by `appId` for the main view concatenated with `viewId` for other views. -All the types use the shared fields `appId: 'keyword'`, `viewId: 'keyword'`, `numberOfClicks: 'long'` and `minutesOnScreen: 'float'`, but they are currently not added in the mappings because we don't use them for search purposes, and we need to be thoughtful with the number of mapped fields in the SavedObjects index ([#43673](https://github.com/elastic/kibana/issues/43673)). `application_usage_transactional` and `application_usage_daily` also store `timestamp: { type: 'date' }`. +All the types use the shared fields `appId: 'keyword'`, `viewId: 'keyword'`, `numberOfClicks: 'long'` and `minutesOnScreen: 'float'`, but they are currently not added in the mappings because we don't use them for search purposes, and we need to be thoughtful with the number of mapped fields in the SavedObjects index ([#43673](https://github.com/elastic/kibana/issues/43673)). The SO type `application_usage_transactional` also stores `timestamp: { type: 'date' }`. Rollups uses `appId` in the savedObject id for the default view. For other views `viewId` is concatenated. This keeps backwards compatiblity with previously stored documents on the clusters without requiring any form of migration. diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts index f072f044925bf..1706ec195e577 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/constants.ts @@ -11,11 +11,6 @@ */ export const ROLL_TOTAL_INDICES_INTERVAL = 24 * 60 * 60 * 1000; -/** - * Roll daily indices every 24h - */ -export const ROLL_DAILY_INDICES_INTERVAL = 24 * 60 * 60 * 1000; - /** * Start rolling indices after 5 minutes up */ diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts index 2d2d07d9d1894..676f5fddc16e1 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.ts @@ -7,4 +7,3 @@ */ export { registerApplicationUsageCollector } from './telemetry_application_usage_collector'; -export { rollDailyData as migrateTransactionalDocs } from './rollups'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.test.ts deleted file mode 100644 index 9c0fab85844bb..0000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { savedObjectsRepositoryMock, loggingSystemMock } from '@kbn/core/server/mocks'; -import { SavedObjectsErrorHelpers } from '@kbn/core/server'; -import { SAVED_OBJECTS_DAILY_TYPE, SAVED_OBJECTS_TRANSACTIONAL_TYPE } from '../saved_objects_types'; -import { rollDailyData } from './daily'; - -describe('rollDailyData', () => { - const logger = loggingSystemMock.createLogger(); - - test('returns false if no savedObjectsClient initialised yet', async () => { - await expect(rollDailyData(logger, undefined)).resolves.toBe(false); - }); - - test('handle empty results', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case SAVED_OBJECTS_TRANSACTIONAL_TYPE: - return { saved_objects: [], total: 0, page, per_page: perPage }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(true); - expect(savedObjectClient.get).not.toBeCalled(); - expect(savedObjectClient.bulkCreate).not.toBeCalled(); - expect(savedObjectClient.delete).not.toBeCalled(); - }); - - test('migrate some docs', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - let timesCalled = 0; - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case SAVED_OBJECTS_TRANSACTIONAL_TYPE: - if (timesCalled++ > 0) { - return { saved_objects: [], total: 0, page, per_page: perPage }; - } - return { - saved_objects: [ - { - id: 'test-id-1', - type, - score: 0, - references: [], - attributes: { - appId: 'appId', - timestamp: '2020-01-01T10:31:00.000Z', - minutesOnScreen: 0.5, - numberOfClicks: 1, - }, - }, - { - id: 'test-id-2', - type, - score: 0, - references: [], - attributes: { - appId: 'appId', - timestamp: '2020-01-01T11:31:00.000Z', - minutesOnScreen: 2.5, - numberOfClicks: 2, - }, - }, - { - id: 'test-id-3', - type, - score: 0, - references: [], - attributes: { - appId: 'appId', - viewId: 'appId_viewId', - timestamp: '2020-01-01T11:31:00.000Z', - minutesOnScreen: 1, - numberOfClicks: 5, - }, - }, - ], - total: 3, - page, - per_page: perPage, - }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - - savedObjectClient.get.mockImplementation(async (type, id) => { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - }); - - await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(true); - expect(savedObjectClient.get).toHaveBeenCalledTimes(2); - expect(savedObjectClient.get).toHaveBeenNthCalledWith( - 1, - SAVED_OBJECTS_DAILY_TYPE, - 'appId:2020-01-01' - ); - expect(savedObjectClient.get).toHaveBeenNthCalledWith( - 2, - SAVED_OBJECTS_DAILY_TYPE, - 'appId:2020-01-01:appId_viewId' - ); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(1); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith( - [ - { - type: SAVED_OBJECTS_DAILY_TYPE, - id: 'appId:2020-01-01', - attributes: { - appId: 'appId', - viewId: undefined, - timestamp: '2020-01-01T00:00:00.000Z', - minutesOnScreen: 3.0, - numberOfClicks: 3, - }, - }, - { - type: SAVED_OBJECTS_DAILY_TYPE, - id: 'appId:2020-01-01:appId_viewId', - attributes: { - appId: 'appId', - viewId: 'appId_viewId', - timestamp: '2020-01-01T00:00:00.000Z', - minutesOnScreen: 1.0, - numberOfClicks: 5, - }, - }, - ], - { overwrite: true } - ); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(3); - expect(savedObjectClient.delete).toHaveBeenNthCalledWith( - 1, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, - 'test-id-1' - ); - expect(savedObjectClient.delete).toHaveBeenNthCalledWith( - 2, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, - 'test-id-2' - ); - expect(savedObjectClient.delete).toHaveBeenNthCalledWith( - 3, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, - 'test-id-3' - ); - }); - - test('error getting the daily document', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - let timesCalled = 0; - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case SAVED_OBJECTS_TRANSACTIONAL_TYPE: - if (timesCalled++ > 0) { - return { saved_objects: [], total: 0, page, per_page: perPage }; - } - return { - saved_objects: [ - { - id: 'test-id-1', - type, - score: 0, - references: [], - attributes: { - appId: 'appId', - timestamp: '2020-01-01T10:31:00.000Z', - minutesOnScreen: 0.5, - numberOfClicks: 1, - }, - }, - ], - total: 1, - page, - per_page: perPage, - }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - - savedObjectClient.get.mockImplementation(async (type, id) => { - throw new Error('Something went terribly wrong'); - }); - - await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(false); - expect(savedObjectClient.get).toHaveBeenCalledTimes(1); - expect(savedObjectClient.get).toHaveBeenCalledWith( - SAVED_OBJECTS_DAILY_TYPE, - 'appId:2020-01-01' - ); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(0); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(0); - }); -}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.ts deleted file mode 100644 index 7cd326eeec346..0000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/daily.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 moment from 'moment'; -import type { Logger } from '@kbn/logging'; -import { ISavedObjectsRepository, SavedObject, SavedObjectsErrorHelpers } from '@kbn/core/server'; -import { getDailyId } from '@kbn/usage-collection-plugin/common/application_usage'; -import { - ApplicationUsageDaily, - ApplicationUsageTransactional, - SAVED_OBJECTS_DAILY_TYPE, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, -} from '../saved_objects_types'; - -/** - * For Rolling the daily data, we only care about the stored attributes and the version (to avoid overwriting via concurrent requests) - */ -type ApplicationUsageDailyWithVersion = Pick< - SavedObject, - 'version' | 'attributes' ->; - -/** - * Aggregates all the transactional events into daily aggregates - * @param logger - * @param savedObjectsClient - */ -export async function rollDailyData( - logger: Logger, - savedObjectsClient?: ISavedObjectsRepository -): Promise { - if (!savedObjectsClient) { - return false; - } - - try { - let toCreate: Map; - do { - toCreate = new Map(); - const { saved_objects: rawApplicationUsageTransactional } = - await savedObjectsClient.find({ - type: SAVED_OBJECTS_TRANSACTIONAL_TYPE, - perPage: 1000, // Process 1000 at a time as a compromise of speed and overload - }); - - for (const doc of rawApplicationUsageTransactional) { - const { - attributes: { appId, viewId, minutesOnScreen, numberOfClicks, timestamp }, - } = doc; - const dayId = moment(timestamp).format('YYYY-MM-DD'); - - const dailyId = getDailyId({ dayId, appId, viewId }); - - const existingDoc = - toCreate.get(dailyId) || - (await getDailyDoc(savedObjectsClient, dailyId, appId, viewId, dayId)); - toCreate.set(dailyId, { - ...existingDoc, - attributes: { - ...existingDoc.attributes, - minutesOnScreen: existingDoc.attributes.minutesOnScreen + minutesOnScreen, - numberOfClicks: existingDoc.attributes.numberOfClicks + numberOfClicks, - }, - }); - } - if (toCreate.size > 0) { - await savedObjectsClient.bulkCreate( - [...toCreate.entries()].map(([id, { attributes, version }]) => ({ - type: SAVED_OBJECTS_DAILY_TYPE, - id, - attributes, - version, // Providing version to ensure via conflict matching that only 1 Kibana instance (or interval) is taking care of the updates - })), - { overwrite: true } - ); - const promiseStatuses = await Promise.allSettled( - rawApplicationUsageTransactional.map( - ({ id }) => savedObjectsClient.delete(SAVED_OBJECTS_TRANSACTIONAL_TYPE, id) // There is no bulkDelete :( - ) - ); - const rejectedPromises = promiseStatuses.filter( - (settledResult): settledResult is PromiseRejectedResult => - settledResult.status === 'rejected' - ); - if (rejectedPromises.length > 0) { - throw new Error( - `Failed to delete some items in ${SAVED_OBJECTS_TRANSACTIONAL_TYPE}: ${JSON.stringify( - rejectedPromises.map(({ reason }) => reason) - )}` - ); - } - } - } while (toCreate.size > 0); - return true; - } catch (err) { - logger.debug(`Failed to rollup transactional to daily entries`); - logger.debug(err); - return false; - } -} - -/** - * Gets daily doc from the SavedObjects repository. Creates a new one if not found - * @param savedObjectsClient - * @param id The ID of the document to retrieve (typically, `${appId}:${dayId}`) - * @param appId The application ID - * @param viewId The application view ID - * @param dayId The date of the document in the format YYYY-MM-DD - */ -async function getDailyDoc( - savedObjectsClient: ISavedObjectsRepository, - id: string, - appId: string, - viewId: string, - dayId: string -): Promise { - try { - const { attributes, version } = await savedObjectsClient.get( - SAVED_OBJECTS_DAILY_TYPE, - id - ); - return { attributes, version }; - } catch (err) { - if (SavedObjectsErrorHelpers.isNotFoundError(err)) { - return { - attributes: { - appId, - viewId, - // Concatenating the day in YYYY-MM-DD form to T00:00:00Z to reduce the TZ effects - timestamp: moment(`${moment(dayId).format('YYYY-MM-DD')}T00:00:00Z`).toISOString(), - minutesOnScreen: 0, - numberOfClicks: 0, - }, - }; - } - throw err; - } -} diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts index 8f3d83613aa9d..484036841b8f7 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups/index.ts @@ -6,6 +6,5 @@ * Side Public License, v 1. */ -export { rollDailyData } from './daily'; export { rollTotals } from './total'; export { serializeKey } from './utils'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts index 15856c21760ce..5a75cea43d88c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts @@ -19,12 +19,8 @@ import { SAVED_OBJECTS_TOTAL_TYPE, } from './saved_objects_types'; import { applicationUsageSchema } from './schema'; -import { rollTotals, rollDailyData, serializeKey } from './rollups'; -import { - ROLL_TOTAL_INDICES_INTERVAL, - ROLL_DAILY_INDICES_INTERVAL, - ROLL_INDICES_START, -} from './constants'; +import { rollTotals, serializeKey } from './rollups'; +import { ROLL_TOTAL_INDICES_INTERVAL, ROLL_INDICES_START } from './constants'; import { ApplicationUsageTelemetryReport, ApplicationUsageViews } from './types'; export const transformByApplicationViews = ( @@ -60,17 +56,6 @@ export function registerApplicationUsageCollector( rollTotals(logger, getSavedObjectsClient()) ); - const dailyRollingSub = timer(ROLL_INDICES_START, ROLL_DAILY_INDICES_INTERVAL).subscribe( - async () => { - const success = await rollDailyData(logger, getSavedObjectsClient()); - // we only need to roll the transactional documents once to assure BWC - // once we rolling succeeds, we can stop. - if (success) { - dailyRollingSub.unsubscribe(); - } - } - ); - const collector = usageCollection.makeUsageCollector( { type: 'application_usage', diff --git a/src/plugins/kibana_usage_collection/server/collectors/index.ts b/src/plugins/kibana_usage_collection/server/collectors/index.ts index e4ed24611bfa8..6de234b5de434 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/index.ts @@ -19,11 +19,7 @@ export { registerCspCollector } from './csp'; export { registerCoreUsageCollector } from './core'; export { registerLocalizationUsageCollector } from './localization'; export { registerConfigUsageCollector } from './config_usage'; -export { - registerUiCountersUsageCollector, - registerUiCounterSavedObjectType, - registerUiCountersRollups, -} from './ui_counters'; +export { registerUiCountersUsageCollector } from './ui_counters'; export { registerUsageCountersRollups, registerUsageCountersUsageCollector, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 7a19ff022226e..a948a035f2d48 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -418,6 +418,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:enableNewSyntheticsView': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'observability:maxSuggestions': { type: 'integer', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index b9d50f888fa93..718f75b80a77d 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -36,6 +36,7 @@ export interface UsageStats { 'discover:maxDocFieldsDisplayed': number; 'securitySolution:rulesTableRefresh': string; 'observability:enableInspectEsQueries': boolean; + 'observability:enableNewSyntheticsView': boolean; 'observability:maxSuggestions': number; 'observability:enableComparisonByDefault': boolean; 'observability:enableInfrastructureView': boolean; diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/__fixtures__/ui_counter_saved_objects.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/__fixtures__/ui_counter_saved_objects.ts deleted file mode 100644 index ebc958c7be8c6..0000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/__fixtures__/ui_counter_saved_objects.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { UICounterSavedObject } from '../ui_counter_saved_object_type'; -export const rawUiCounters: UICounterSavedObject[] = [ - { - type: 'ui-counter', - id: 'Kibana_home:23102020:click:different_type', - attributes: { - count: 1, - }, - references: [], - updated_at: '2020-11-24T11:27:57.067Z', - version: 'WzI5NDRd', - }, - { - type: 'ui-counter', - id: 'Kibana_home:25102020:loaded:intersecting_event', - attributes: { - count: 1, - }, - references: [], - updated_at: '2020-10-25T11:27:57.067Z', - version: 'WzI5NDRd', - }, - { - type: 'ui-counter', - id: 'Kibana_home:23102020:loaded:intersecting_event', - attributes: { - count: 3, - }, - references: [], - updated_at: '2020-10-23T11:27:57.067Z', - version: 'WzI5NDRd', - }, - { - type: 'ui-counter', - id: 'Kibana_home:24112020:click:only_reported_in_ui_counters', - attributes: { - count: 1, - }, - references: [], - updated_at: '2020-11-24T11:27:57.067Z', - version: 'WzI5NDRd', - }, -]; diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/index.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/index.ts index 795e4a75aa236..cc547266c618d 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/index.ts @@ -7,5 +7,3 @@ */ export { registerUiCountersUsageCollector } from './register_ui_counters_collector'; -export { registerUiCounterSavedObjectType } from './ui_counter_saved_object_type'; -export { registerUiCountersRollups } from './rollups'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.test.ts index 9d702be86aa48..0e84df3325d3d 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.test.ts @@ -6,16 +6,9 @@ * Side Public License, v 1. */ -import { - transformRawUiCounterObject, - transformRawUsageCounterObject, - createFetchUiCounters, -} from './register_ui_counters_collector'; -import { BehaviorSubject } from 'rxjs'; -import { rawUiCounters } from './__fixtures__/ui_counter_saved_objects'; +import { transformRawUsageCounterObject, fetchUiCounters } from './register_ui_counters_collector'; import { rawUsageCounters } from './__fixtures__/usage_counter_saved_objects'; import { savedObjectsClientMock } from '@kbn/core/server/mocks'; -import { UI_COUNTER_SAVED_OBJECT_TYPE } from './ui_counter_saved_object_type'; import { USAGE_COUNTERS_SAVED_OBJECT_TYPE } from '@kbn/usage-collection-plugin/server'; describe('transformRawUsageCounterObject', () => { @@ -63,84 +56,16 @@ describe('transformRawUsageCounterObject', () => { }); }); -describe('transformRawUiCounterObject', () => { - it('transforms ui counters savedObject raw entries', () => { - const result = rawUiCounters.map(transformRawUiCounterObject); - expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "appName": "Kibana_home", - "counterType": "click", - "eventName": "different_type", - "fromTimestamp": "2020-11-24T00:00:00Z", - "lastUpdatedAt": "2020-11-24T11:27:57.067Z", - "total": 1, - }, - Object { - "appName": "Kibana_home", - "counterType": "loaded", - "eventName": "intersecting_event", - "fromTimestamp": "2020-10-25T00:00:00Z", - "lastUpdatedAt": "2020-10-25T11:27:57.067Z", - "total": 1, - }, - Object { - "appName": "Kibana_home", - "counterType": "loaded", - "eventName": "intersecting_event", - "fromTimestamp": "2020-10-23T00:00:00Z", - "lastUpdatedAt": "2020-10-23T11:27:57.067Z", - "total": 3, - }, - Object { - "appName": "Kibana_home", - "counterType": "click", - "eventName": "only_reported_in_ui_counters", - "fromTimestamp": "2020-11-24T00:00:00Z", - "lastUpdatedAt": "2020-11-24T11:27:57.067Z", - "total": 1, - }, - ] - `); - }); -}); - -describe('createFetchUiCounters', () => { - let stopUsingUiCounterIndicies$: BehaviorSubject; +describe('fetchUiCounters', () => { const soClientMock = savedObjectsClientMock.create(); beforeEach(() => { jest.clearAllMocks(); - stopUsingUiCounterIndicies$ = new BehaviorSubject(false); - }); - - it('does not query ui_counters saved objects if stopUsingUiCounterIndicies$ is complete', async () => { - // @ts-expect-error incomplete mock implementation - soClientMock.find.mockImplementation(async ({ type }) => { - switch (type) { - case USAGE_COUNTERS_SAVED_OBJECT_TYPE: - return { saved_objects: rawUsageCounters }; - default: - throw new Error(`unexpected type ${type}`); - } - }); - - stopUsingUiCounterIndicies$.complete(); - // @ts-expect-error incomplete mock implementation - const { dailyEvents } = await createFetchUiCounters(stopUsingUiCounterIndicies$)({ - soClient: soClientMock, - }); - - const transforemdUsageCounters = rawUsageCounters.map(transformRawUsageCounterObject); - expect(soClientMock.find).toBeCalledTimes(1); - expect(dailyEvents).toEqual(transforemdUsageCounters.filter(Boolean)); }); - it('merges saved objects from both ui_counters and usage_counters saved objects', async () => { + it('returns saved objects only from usage_counters saved objects', async () => { // @ts-expect-error incomplete mock implementation soClientMock.find.mockImplementation(async ({ type }) => { switch (type) { - case UI_COUNTER_SAVED_OBJECT_TYPE: - return { saved_objects: rawUiCounters }; case USAGE_COUNTERS_SAVED_OBJECT_TYPE: return { saved_objects: rawUsageCounters }; default: @@ -149,10 +74,10 @@ describe('createFetchUiCounters', () => { }); // @ts-expect-error incomplete mock implementation - const { dailyEvents } = await createFetchUiCounters(stopUsingUiCounterIndicies$)({ + const { dailyEvents } = await fetchUiCounters({ soClient: soClientMock, }); - expect(dailyEvents).toHaveLength(7); + expect(dailyEvents).toHaveLength(4); const intersectingEntry = dailyEvents.find( ({ eventName, fromTimestamp }) => eventName === 'intersecting_event' && fromTimestamp === '2020-10-23T00:00:00Z' @@ -179,16 +104,7 @@ describe('createFetchUiCounters', () => { expect(invalidCountEntry).toBe(undefined); expect(nonUiCountersEntry).toBe(undefined); expect(zeroCountEntry).toBe(undefined); - expect(onlyFromUICountersEntry).toMatchInlineSnapshot(` - Object { - "appName": "Kibana_home", - "counterType": "click", - "eventName": "only_reported_in_ui_counters", - "fromTimestamp": "2020-11-24T00:00:00Z", - "lastUpdatedAt": "2020-11-24T11:27:57.067Z", - "total": 1, - } - `); + expect(onlyFromUICountersEntry).toBe(undefined); expect(onlyFromUsageCountersEntry).toMatchInlineSnapshot(` Object { "appName": "myApp", @@ -206,7 +122,7 @@ describe('createFetchUiCounters', () => { "eventName": "intersecting_event", "fromTimestamp": "2020-10-23T00:00:00Z", "lastUpdatedAt": "2020-10-23T11:27:57.067Z", - "total": 63, + "total": 60, } `); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.ts index 5d741a6df8e3d..c2e17c24de488 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.ts @@ -7,8 +7,6 @@ */ import moment from 'moment'; -import { mergeWith } from 'lodash'; -import type { Subject } from 'rxjs'; import { CollectorFetchContext, @@ -16,18 +14,9 @@ import { USAGE_COUNTERS_SAVED_OBJECT_TYPE, UsageCountersSavedObject, UsageCountersSavedObjectAttributes, - serializeCounterKey, } from '@kbn/usage-collection-plugin/server'; -import { - deserializeUiCounterName, - serializeUiCounterName, -} from '@kbn/usage-collection-plugin/common/ui_counters'; -import { - UICounterSavedObject, - UICounterSavedObjectAttributes, - UI_COUNTER_SAVED_OBJECT_TYPE, -} from './ui_counter_saved_object_type'; +import { deserializeUiCounterName } from '@kbn/usage-collection-plugin/common/ui_counters'; interface UiCounterEvent { appName: string; @@ -42,32 +31,6 @@ export interface UiCountersUsage { dailyEvents: UiCounterEvent[]; } -export function transformRawUiCounterObject( - rawUiCounter: UICounterSavedObject -): UiCounterEvent | undefined { - const { - id, - attributes: { count }, - updated_at: lastUpdatedAt, - } = rawUiCounter; - if (typeof count !== 'number' || count < 1) { - return; - } - - const [appName, , counterType, ...restId] = id.split(':'); - const eventName = restId.join(':'); - const fromTimestamp = moment(lastUpdatedAt).utc().startOf('day').format(); - - return { - appName, - eventName, - lastUpdatedAt, - fromTimestamp, - counterType, - total: count, - }; -} - export function transformRawUsageCounterObject( rawUsageCounter: UsageCountersSavedObject ): UiCounterEvent | undefined { @@ -93,80 +56,33 @@ export function transformRawUsageCounterObject( }; } -export const createFetchUiCounters = (stopUsingUiCounterIndicies$: Subject) => - async function fetchUiCounters({ soClient }: CollectorFetchContext) { - const { saved_objects: rawUsageCounters } = - await soClient.find({ - type: USAGE_COUNTERS_SAVED_OBJECT_TYPE, - fields: ['count', 'counterName', 'counterType', 'domainId'], - filter: `${USAGE_COUNTERS_SAVED_OBJECT_TYPE}.attributes.domainId: uiCounter`, - perPage: 10000, - }); - - const skipFetchingUiCounters = stopUsingUiCounterIndicies$.isStopped; - const result = - skipFetchingUiCounters || - (await soClient.find({ - type: UI_COUNTER_SAVED_OBJECT_TYPE, - fields: ['count'], - perPage: 10000, - })); +export async function fetchUiCounters({ soClient }: CollectorFetchContext) { + const { saved_objects: rawUsageCounters } = + await soClient.find({ + type: USAGE_COUNTERS_SAVED_OBJECT_TYPE, + fields: ['count', 'counterName', 'counterType', 'domainId'], + filter: `${USAGE_COUNTERS_SAVED_OBJECT_TYPE}.attributes.domainId: uiCounter`, + perPage: 10000, + }); - const rawUiCounters = typeof result === 'object' ? result.saved_objects : []; - const dailyEventsFromUiCounters = rawUiCounters.reduce((acc, raw) => { - try { - const event = transformRawUiCounterObject(raw); - if (event) { - const { appName, eventName, counterType } = event; - const key = serializeCounterKey({ - domainId: 'uiCounter', - counterName: serializeUiCounterName({ appName, eventName }), - counterType, - date: event.lastUpdatedAt, - }); - - acc[key] = event; - } - } catch (_) { - // swallow error; allows sending successfully transformed objects. - } - return acc; - }, {} as Record); - - const dailyEventsFromUsageCounters = rawUsageCounters.reduce((acc, raw) => { - try { - const event = transformRawUsageCounterObject(raw); - if (event) { - acc[raw.id] = event; - } - } catch (_) { - // swallow error; allows sending successfully transformed objects. - } - return acc; - }, {} as Record); - - const mergedDailyCounters = mergeWith( - dailyEventsFromUsageCounters, - dailyEventsFromUiCounters, - (value: UiCounterEvent | undefined, srcValue: UiCounterEvent): UiCounterEvent => { - if (!value) { - return srcValue; + return { + dailyEvents: Object.values( + rawUsageCounters.reduce((acc, raw) => { + try { + const event = transformRawUsageCounterObject(raw); + if (event) { + acc[raw.id] = event; + } + } catch (_) { + // swallow error; allows sending successfully transformed objects. } - - return { - ...srcValue, - total: srcValue.total + value.total, - }; - } - ); - - return { dailyEvents: Object.values(mergedDailyCounters) }; + return acc; + }, {} as Record) + ), }; +} -export function registerUiCountersUsageCollector( - usageCollection: UsageCollectionSetup, - stopUsingUiCounterIndicies$: Subject -) { +export function registerUiCountersUsageCollector(usageCollection: UsageCollectionSetup) { const collector = usageCollection.makeUsageCollector({ type: 'ui_counters', schema: { @@ -197,7 +113,7 @@ export function registerUiCountersUsageCollector( }, }, }, - fetch: createFetchUiCounters(stopUsingUiCounterIndicies$), + fetch: fetchUiCounters, isReady: () => true, }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/register_rollups.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/register_rollups.ts deleted file mode 100644 index 859a50e01401a..0000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/register_rollups.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Subject, timer } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; -import { Logger, ISavedObjectsRepository } from '@kbn/core/server'; -import { ROLL_INDICES_INTERVAL, ROLL_INDICES_START } from './constants'; -import { rollUiCounterIndices } from './rollups'; - -export function registerUiCountersRollups( - logger: Logger, - stopRollingUiCounterIndicies$: Subject, - getSavedObjectsClient: () => ISavedObjectsRepository | undefined -) { - timer(ROLL_INDICES_START, ROLL_INDICES_INTERVAL) - .pipe(takeUntil(stopRollingUiCounterIndicies$)) - .subscribe(() => - rollUiCounterIndices(logger, stopRollingUiCounterIndicies$, getSavedObjectsClient()) - ); -} diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/rollups.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/rollups.test.ts deleted file mode 100644 index e5414ed0d5001..0000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/rollups.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import moment from 'moment'; -import * as Rx from 'rxjs'; -import { isSavedObjectOlderThan, rollUiCounterIndices } from './rollups'; -import { savedObjectsRepositoryMock, loggingSystemMock } from '@kbn/core/server/mocks'; -import { SavedObjectsFindResult } from '@kbn/core/server'; - -import { - UICounterSavedObjectAttributes, - UI_COUNTER_SAVED_OBJECT_TYPE, -} from '../ui_counter_saved_object_type'; -import { UI_COUNTERS_KEEP_DOCS_FOR_DAYS } from './constants'; - -const createMockSavedObjectDoc = (updatedAt: moment.Moment, id: string) => - ({ - id, - type: 'ui-counter', - attributes: { - count: 3, - }, - references: [], - updated_at: updatedAt.format(), - version: 'WzI5LDFd', - score: 0, - } as SavedObjectsFindResult); - -describe('isSavedObjectOlderThan', () => { - it(`returns true if doc is older than x days`, () => { - const numberOfDays = 1; - const startDate = moment().format(); - const doc = createMockSavedObjectDoc(moment().subtract(2, 'days'), 'some-id'); - const result = isSavedObjectOlderThan({ - numberOfDays, - startDate, - doc, - }); - expect(result).toBe(true); - }); - - it(`returns false if doc is exactly x days old`, () => { - const numberOfDays = 1; - const startDate = moment().format(); - const doc = createMockSavedObjectDoc(moment().subtract(1, 'days'), 'some-id'); - const result = isSavedObjectOlderThan({ - numberOfDays, - startDate, - doc, - }); - expect(result).toBe(false); - }); - - it(`returns false if doc is younger than x days`, () => { - const numberOfDays = 2; - const startDate = moment().format(); - const doc = createMockSavedObjectDoc(moment().subtract(1, 'days'), 'some-id'); - const result = isSavedObjectOlderThan({ - numberOfDays, - startDate, - doc, - }); - expect(result).toBe(false); - }); -}); - -describe('rollUiCounterIndices', () => { - let logger: ReturnType; - let savedObjectClient: ReturnType; - let stopUsingUiCounterIndicies$: Rx.Subject; - - beforeEach(() => { - logger = loggingSystemMock.createLogger(); - savedObjectClient = savedObjectsRepositoryMock.create(); - stopUsingUiCounterIndicies$ = new Rx.Subject(); - }); - - it('returns undefined if no savedObjectsClient initialised yet', async () => { - await expect( - rollUiCounterIndices(logger, stopUsingUiCounterIndicies$, undefined) - ).resolves.toBe(undefined); - expect(logger.warn).toHaveBeenCalledTimes(0); - }); - - it('does not delete any documents on empty saved objects', async () => { - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case UI_COUNTER_SAVED_OBJECT_TYPE: - return { saved_objects: [], total: 0, page, per_page: perPage }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - await expect( - rollUiCounterIndices(logger, stopUsingUiCounterIndicies$, savedObjectClient) - ).resolves.toEqual([]); - expect(savedObjectClient.find).toBeCalled(); - expect(savedObjectClient.delete).not.toBeCalled(); - expect(logger.warn).toHaveBeenCalledTimes(0); - }); - it('calls Subject complete() on empty saved objects', async () => { - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case UI_COUNTER_SAVED_OBJECT_TYPE: - return { saved_objects: [], total: 0, page, per_page: perPage }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - await expect( - rollUiCounterIndices(logger, stopUsingUiCounterIndicies$, savedObjectClient) - ).resolves.toEqual([]); - expect(stopUsingUiCounterIndicies$.isStopped).toBe(true); - }); - - it(`deletes documents older than ${UI_COUNTERS_KEEP_DOCS_FOR_DAYS} days`, async () => { - const mockSavedObjects = [ - createMockSavedObjectDoc(moment().subtract(5, 'days'), 'doc-id-1'), - createMockSavedObjectDoc(moment().subtract(1, 'days'), 'doc-id-2'), - createMockSavedObjectDoc(moment().subtract(6, 'days'), 'doc-id-3'), - ]; - - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case UI_COUNTER_SAVED_OBJECT_TYPE: - return { saved_objects: mockSavedObjects, total: 0, page, per_page: perPage }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - await expect( - rollUiCounterIndices(logger, stopUsingUiCounterIndicies$, savedObjectClient) - ).resolves.toHaveLength(2); - expect(savedObjectClient.find).toBeCalled(); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(2); - expect(savedObjectClient.delete).toHaveBeenNthCalledWith( - 1, - UI_COUNTER_SAVED_OBJECT_TYPE, - 'doc-id-1' - ); - expect(savedObjectClient.delete).toHaveBeenNthCalledWith( - 2, - UI_COUNTER_SAVED_OBJECT_TYPE, - 'doc-id-3' - ); - expect(logger.warn).toHaveBeenCalledTimes(0); - }); - - it(`logs warnings on savedObject.find failure`, async () => { - savedObjectClient.find.mockImplementation(async () => { - throw new Error(`Expected error!`); - }); - await expect( - rollUiCounterIndices(logger, stopUsingUiCounterIndicies$, savedObjectClient) - ).resolves.toEqual(undefined); - expect(savedObjectClient.find).toBeCalled(); - expect(savedObjectClient.delete).not.toBeCalled(); - expect(logger.warn).toHaveBeenCalledTimes(2); - }); - - it(`logs warnings on savedObject.delete failure`, async () => { - const mockSavedObjects = [createMockSavedObjectDoc(moment().subtract(5, 'days'), 'doc-id-1')]; - - savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { - switch (type) { - case UI_COUNTER_SAVED_OBJECT_TYPE: - return { saved_objects: mockSavedObjects, total: 0, page, per_page: perPage }; - default: - throw new Error(`Unexpected type [${type}]`); - } - }); - savedObjectClient.delete.mockImplementation(async () => { - throw new Error(`Expected error!`); - }); - await expect( - rollUiCounterIndices(logger, stopUsingUiCounterIndicies$, savedObjectClient) - ).resolves.toEqual(undefined); - expect(savedObjectClient.find).toBeCalled(); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(1); - expect(savedObjectClient.delete).toHaveBeenNthCalledWith( - 1, - UI_COUNTER_SAVED_OBJECT_TYPE, - 'doc-id-1' - ); - expect(logger.warn).toHaveBeenCalledTimes(2); - }); -}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/rollups.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/rollups.ts deleted file mode 100644 index ca472fe0825f9..0000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/rollups.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ISavedObjectsRepository, Logger } from '@kbn/core/server'; -import moment from 'moment'; -import type { Subject } from 'rxjs'; - -import { UI_COUNTERS_KEEP_DOCS_FOR_DAYS } from './constants'; -import { - UICounterSavedObject, - UI_COUNTER_SAVED_OBJECT_TYPE, -} from '../ui_counter_saved_object_type'; - -export function isSavedObjectOlderThan({ - numberOfDays, - startDate, - doc, -}: { - numberOfDays: number; - startDate: moment.Moment | string | number; - doc: Pick; -}): boolean { - const { updated_at: updatedAt } = doc; - const today = moment(startDate).startOf('day'); - const updateDay = moment(updatedAt).startOf('day'); - - const diffInDays = today.diff(updateDay, 'days'); - if (diffInDays > numberOfDays) { - return true; - } - - return false; -} - -export async function rollUiCounterIndices( - logger: Logger, - stopUsingUiCounterIndicies$: Subject, - savedObjectsClient?: ISavedObjectsRepository -) { - if (!savedObjectsClient) { - return; - } - - const now = moment(); - - try { - const { saved_objects: rawUiCounterDocs } = await savedObjectsClient.find( - { - type: UI_COUNTER_SAVED_OBJECT_TYPE, - perPage: 1000, // Process 1000 at a time as a compromise of speed and overload - } - ); - - if (rawUiCounterDocs.length === 0) { - /** - * @deprecated 7.13 to be removed in 8.0.0 - * Stop triggering rollups when we've rolled up all documents. - * - * This Saved Object registry is no longer used. - * Migration from one SO registry to another is not yet supported. - * In a future release we can remove this piece of code and - * migrate any docs to the Usage Counters Saved object. - * - * @removeBy 8.0.0 - */ - - stopUsingUiCounterIndicies$.complete(); - } - - const docsToDelete = rawUiCounterDocs.filter((doc) => - isSavedObjectOlderThan({ - numberOfDays: UI_COUNTERS_KEEP_DOCS_FOR_DAYS, - startDate: now, - doc, - }) - ); - - return await Promise.all( - docsToDelete.map(({ id }) => savedObjectsClient.delete(UI_COUNTER_SAVED_OBJECT_TYPE, id)) - ); - } catch (err) { - logger.warn(`Failed to rollup UI Counters saved objects.`); - logger.warn(err); - } -} diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/ui_counter_saved_object_type.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_counters/ui_counter_saved_object_type.ts deleted file mode 100644 index 2d4e680a61a2f..0000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/ui_counter_saved_object_type.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { SavedObject, SavedObjectAttributes, SavedObjectsServiceSetup } from '@kbn/core/server'; - -export interface UICounterSavedObjectAttributes extends SavedObjectAttributes { - count: number; -} - -export type UICounterSavedObject = SavedObject; - -export const UI_COUNTER_SAVED_OBJECT_TYPE = 'ui-counter'; - -export function registerUiCounterSavedObjectType(savedObjectsSetup: SavedObjectsServiceSetup) { - savedObjectsSetup.registerType({ - name: UI_COUNTER_SAVED_OBJECT_TYPE, - hidden: false, - namespaceType: 'agnostic', - mappings: { - properties: { - count: { type: 'integer' }, - }, - }, - }); -} diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index 4442757e2df68..34bf029311307 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -37,8 +37,6 @@ import { registerCoreUsageCollector, registerLocalizationUsageCollector, registerUiCountersUsageCollector, - registerUiCounterSavedObjectType, - registerUiCountersRollups, registerConfigUsageCollector, registerUsageCountersRollups, registerUsageCountersUsageCollector, @@ -125,9 +123,7 @@ export class KibanaUsageCollectionPlugin implements Plugin { const getUiSettingsClient = () => this.uiSettingsClient; const getCoreUsageDataService = () => this.coreUsageData!; - registerUiCounterSavedObjectType(coreSetup.savedObjects); - registerUiCountersRollups(this.logger.get('ui-counters'), pluginStop$, getSavedObjectsClient); - registerUiCountersUsageCollector(usageCollection, pluginStop$); + registerUiCountersUsageCollector(usageCollection); registerUsageCountersRollups(this.logger.get('usage-counters-rollup'), getSavedObjectsClient); registerUsageCountersUsageCollector(usageCollection); diff --git a/src/plugins/management/public/components/landing/landing.tsx b/src/plugins/management/public/components/landing/landing.tsx index 6ebf980420918..0851ba3343eee 100644 --- a/src/plugins/management/public/components/landing/landing.tsx +++ b/src/plugins/management/public/components/landing/landing.tsx @@ -46,7 +46,7 @@ export const ManagementLandingPage = ({

diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index 85a9803ffced6..aee35c1f331c7 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -159,7 +159,6 @@ describe('TopNavMenu', () => { await refresh(); - expect(component.find(WRAPPER_SELECTOR).length).toBe(1); expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1); // menu is rendered outside of the component diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 7eb7365ed79f3..62dc67a3ee941 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -117,15 +117,15 @@ export function TopNavMenu(props: TopNavMenuProps): ReactElement | null { {renderMenu(menuClassName)}
- {renderSearchBar()} + {renderSearchBar()} ); } else { return ( - - {renderMenu(menuClassName)} + <> + {renderMenu(menuClassName)} {renderSearchBar()} - + ); } } diff --git a/src/plugins/telemetry/common/constants.ts b/src/plugins/telemetry/common/constants.ts index 7b7d0a0db5057..78887c27fbcfb 100644 --- a/src/plugins/telemetry/common/constants.ts +++ b/src/plugins/telemetry/common/constants.ts @@ -12,6 +12,12 @@ */ export const REPORT_INTERVAL_MS = 86400000; +/** + * How often we poll for the opt-in status. + * Currently, 10 seconds. + */ +export const OPT_IN_POLL_INTERVAL_MS = 10000; + /** * Key for the localStorage service */ diff --git a/src/plugins/telemetry/public/plugin.test.ts b/src/plugins/telemetry/public/plugin.test.ts index 5d9e03fcbae20..f25bf92340ba6 100644 --- a/src/plugins/telemetry/public/plugin.test.ts +++ b/src/plugins/telemetry/public/plugin.test.ts @@ -6,17 +6,18 @@ * Side Public License, v 1. */ -import { TelemetryPlugin } from './plugin'; +import { ElasticV3BrowserShipper } from '@kbn/analytics-shippers-elastic-v3-browser'; import { coreMock } from '@kbn/core/public/mocks'; import { homePluginMock } from '@kbn/home-plugin/public/mocks'; import { screenshotModePluginMock } from '@kbn/screenshot-mode-plugin/public/mocks'; import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/public'; - -let screenshotMode: ScreenshotModePluginSetup; -let home: HomePublicPluginSetup; +import { TelemetryPlugin } from './plugin'; describe('TelemetryPlugin', () => { + let screenshotMode: ScreenshotModePluginSetup; + let home: HomePublicPluginSetup; + beforeEach(() => { screenshotMode = screenshotModePluginMock.createSetupContract(); home = homePluginMock.createSetupContract(); @@ -56,5 +57,18 @@ describe('TelemetryPlugin', () => { }); }); }); + describe('EBT shipper registration', () => { + it('registers the UI telemetry shipper', () => { + const initializerContext = coreMock.createPluginInitializerContext(); + const coreSetupMock = coreMock.createSetup(); + + new TelemetryPlugin(initializerContext).setup(coreSetupMock, { screenshotMode, home }); + + expect(coreSetupMock.analytics.registerShipper).toHaveBeenCalledWith( + ElasticV3BrowserShipper, + { channelName: 'kibana-browser', version: 'version' } + ); + }); + }); }); }); diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts index 133b2c6157c3c..6fe7fe4138a31 100644 --- a/src/plugins/telemetry/public/plugin.ts +++ b/src/plugins/telemetry/public/plugin.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { ElasticV3BrowserShipper } from '@kbn/analytics-shippers-elastic-v3-browser'; + import type { Plugin, CoreStart, @@ -20,7 +22,7 @@ import type { import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/public'; -import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; +import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services'; import type { TelemetrySavedObjectAttributes, @@ -153,6 +155,11 @@ export class TelemetryPlugin implements Plugin { await this.refreshConfig(); analytics.optIn({ global: { enabled: this.telemetryService!.isOptedIn } }); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 59d7ba693156d..0c0cafad6bec6 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -202,29 +202,29 @@ } } }, - "search": { + "search-session": { "properties": { - "successCount": { + "transientCount": { "type": "long" }, - "errorCount": { + "persistedCount": { "type": "long" }, - "averageDuration": { - "type": "float" + "totalCount": { + "type": "long" } } }, - "search-session": { + "search": { "properties": { - "transientCount": { + "successCount": { "type": "long" }, - "persistedCount": { + "errorCount": { "type": "long" }, - "totalCount": { - "type": "long" + "averageDuration": { + "type": "float" } } }, @@ -8133,6 +8133,12 @@ "description": "Non-default value of setting." } }, + "observability:enableNewSyntheticsView": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "observability:maxSuggestions": { "type": "integer", "_meta": { diff --git a/src/plugins/telemetry/server/plugin.test.ts b/src/plugins/telemetry/server/plugin.test.ts new file mode 100644 index 0000000000000..c31e78722abf5 --- /dev/null +++ b/src/plugins/telemetry/server/plugin.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 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 { ElasticV3ServerShipper } from '@kbn/analytics-shippers-elastic-v3-server'; +import { coreMock } from '@kbn/core/server/mocks'; +import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/server/mocks'; +import { telemetryCollectionManagerPluginMock } from '@kbn/telemetry-collection-manager-plugin/server/mocks'; +import { TelemetryPlugin } from './plugin'; + +describe('TelemetryPlugin', () => { + describe('setup', () => { + describe('EBT shipper registration', () => { + it('registers the Server telemetry shipper', () => { + const initializerContext = coreMock.createPluginInitializerContext(); + const coreSetupMock = coreMock.createSetup(); + + new TelemetryPlugin(initializerContext).setup(coreSetupMock, { + usageCollection: usageCollectionPluginMock.createSetupContract(), + telemetryCollectionManager: telemetryCollectionManagerPluginMock.createSetupContract(), + }); + + expect(coreSetupMock.analytics.registerShipper).toHaveBeenCalledWith( + ElasticV3ServerShipper, + { channelName: 'kibana-server', version: 'version' } + ); + }); + }); + }); +}); diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 7c01b85130977..c3c2604dc9a03 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -8,7 +8,18 @@ import { URL } from 'url'; import type { Observable } from 'rxjs'; -import { firstValueFrom, ReplaySubject } from 'rxjs'; +import { + BehaviorSubject, + firstValueFrom, + ReplaySubject, + exhaustMap, + timer, + distinctUntilChanged, + filter, +} from 'rxjs'; + +import { ElasticV3ServerShipper } from '@kbn/analytics-shippers-elastic-v3-server'; + import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import type { TelemetryCollectionManagerPluginSetup, @@ -33,6 +44,7 @@ import { import type { TelemetryConfigType } from './config'; import { FetcherTask } from './fetcher'; import { getTelemetrySavedObject, TelemetrySavedObject } from './telemetry_repository'; +import { OPT_IN_POLL_INTERVAL_MS } from '../common/constants'; import { getTelemetryOptIn, getTelemetryChannelEndpoint } from '../common/telemetry_config'; interface TelemetryPluginsDepsSetup { @@ -74,6 +86,7 @@ export class TelemetryPlugin implements Plugin; + private readonly isOptedIn$ = new BehaviorSubject(undefined); private readonly isDev: boolean; private readonly fetcherTask: FetcherTask; /** @@ -91,6 +104,17 @@ export class TelemetryPlugin implements Plugin(1); + /** + * Poll for the opt-in status and update the `isOptedIn$` subject. + * @private + */ + private readonly optInPollerSubscription = timer(0, OPT_IN_POLL_INTERVAL_MS) + .pipe( + exhaustMap(() => this.getOptInStatus()), + distinctUntilChanged() + ) + .subscribe((isOptedIn) => this.isOptedIn$.next(isOptedIn)); + private security?: SecurityPluginStart; constructor(initializerContext: PluginInitializerContext) { @@ -102,13 +126,29 @@ export class TelemetryPlugin implements Plugin typeof isOptedIn === 'boolean')) + .subscribe((isOptedIn) => analytics.optIn({ global: { enabled: isOptedIn } })); + const savedObjectsInternalRepository = savedObjects.createInternalRepository(); this.savedObjectsInternalRepository = savedObjectsInternalRepository; this.savedObjectsInternalClient$.next(new SavedObjectsClient(savedObjectsInternalRepository)); @@ -155,31 +200,46 @@ export class TelemetryPlugin implements Plugin { - const internalRepositoryClient = await firstValueFrom(this.savedObjectsInternalClient$); - let telemetrySavedObject: TelemetrySavedObject = false; // if an error occurs while fetching opt-in status, a `false` result indicates that Kibana cannot opt-in - try { - telemetrySavedObject = await getTelemetrySavedObject(internalRepositoryClient); - } catch (err) { - this.logger.debug('Failed to check telemetry opt-in status: ' + err.message); - } - - const config = await firstValueFrom(this.config$); - const allowChangingOptInStatus = config.allowChangingOptInStatus; - const configTelemetryOptIn = typeof config.optIn === 'undefined' ? null : config.optIn; - const currentKibanaVersion = this.currentKibanaVersion; - const isOptedIn = getTelemetryOptIn({ - currentKibanaVersion, - telemetrySavedObject, - allowChangingOptInStatus, - configTelemetryOptIn, - }); - - return isOptedIn === true; - }, + getIsOptedIn: async () => this.isOptedIn$.value === true, }; } + public stop() { + this.optInPollerSubscription.unsubscribe(); + this.isOptedIn$.complete(); + } + + private async getOptInStatus(): Promise { + const internalRepositoryClient = await firstValueFrom(this.savedObjectsInternalClient$); + + let telemetrySavedObject: TelemetrySavedObject | undefined; + try { + telemetrySavedObject = await getTelemetrySavedObject(internalRepositoryClient); + } catch (err) { + this.logger.debug('Failed to check telemetry opt-in status: ' + err.message); + } + + // If we can't get the saved object due to permissions or other error other than 404, skip this round. + if (typeof telemetrySavedObject === 'undefined' || telemetrySavedObject === false) { + return; + } + + const config = await firstValueFrom(this.config$); + const allowChangingOptInStatus = config.allowChangingOptInStatus; + const configTelemetryOptIn = typeof config.optIn === 'undefined' ? null : config.optIn; + const currentKibanaVersion = this.currentKibanaVersion; + const isOptedIn = getTelemetryOptIn({ + currentKibanaVersion, + telemetrySavedObject, + allowChangingOptInStatus, + configTelemetryOptIn, + }); + + if (typeof isOptedIn === 'boolean') { + return isOptedIn; + } + } + private startFetcher( core: CoreStart, telemetryCollectionManager: TelemetryCollectionManagerPluginStart diff --git a/src/plugins/unified_search/.storybook/main.js b/src/plugins/unified_search/.storybook/main.js new file mode 100644 index 0000000000000..8dc3c5d1518f4 --- /dev/null +++ b/src/plugins/unified_search/.storybook/main.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx b/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx new file mode 100644 index 0000000000000..49e25e04d01a8 --- /dev/null +++ b/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx @@ -0,0 +1,431 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import { I18nProvider } from '@kbn/i18n-react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public'; +import { SearchBar, SearchBarProps } from '../search_bar'; +import { setIndexPatterns } from '../services'; + +const mockIndexPatterns = [ + { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], + }, + { + id: '1235', + title: 'test-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], + }, +] as DataView[]; + +const mockTimeHistory = { + get: () => { + return []; + }, + add: action('set'), + get$: () => { + return { + pipe: () => {}, + }; + }, +}; + +const createMockWebStorage = () => ({ + clear: action('clear'), + getItem: action('getItem'), + key: action('key'), + removeItem: action('removeItem'), + setItem: action('setItem'), + length: 0, +}); + +const createMockStorage = () => ({ + storage: createMockWebStorage(), + set: action('set'), + remove: action('remove'), + clear: action('clear'), + get: () => true, +}); + +const services = { + uiSettings: { + get: () => {}, + }, + savedObjects: action('savedObjects'), + notifications: action('notifications'), + http: { + basePath: { + prepend: () => 'http://test', + }, + }, + docLinks: { + links: { + query: { + kueryQuerySyntax: '', + }, + }, + }, + storage: createMockStorage(), + data: { + query: { + savedQueries: { + findSavedQueries: () => + Promise.resolve({ + queries: [ + { + id: 'testwewe', + attributes: { + title: 'Saved query 1', + description: '', + query: { + query: 'category.keyword : "Men\'s Shoes" ', + language: 'kuery', + }, + filters: [], + }, + }, + { + id: '0173d0d0-b19a-11ec-8323-837d6b231b82', + attributes: { + title: 'test', + description: '', + query: { + query: '', + language: 'kuery', + }, + filters: [ + { + meta: { + index: '1234', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }, + }, + ], + }), + }, + }, + autocomplete: { + hasQuerySuggestions: () => Promise.resolve(false), + getQuerySuggestions: () => [], + }, + dataViews: { + getIdsWithTitle: () => [ + { id: '1234', title: 'logstash-*' }, + { id: '1235', title: 'test-*' }, + ], + }, + }, +}; + +setIndexPatterns({ + get: () => Promise.resolve(mockIndexPatterns[0]), +} as unknown as DataViewsContract); + +function wrapSearchBarInContext(testProps: SearchBarProps) { + const defaultOptions = { + appName: 'test', + timeHistory: mockTimeHistory, + intl: null as any, + showQueryBar: true, + showFilterBar: true, + showDatePicker: true, + showAutoRefreshOnly: false, + showSaveQuery: true, + showQueryInput: true, + indexPatterns: mockIndexPatterns, + dateRangeFrom: 'now-15m', + dateRangeTo: 'now', + query: { query: '', language: 'kuery' }, + filters: [], + onClearSavedQuery: action('onClearSavedQuery'), + onFiltersUpdated: action('onFiltersUpdated'), + } as unknown as SearchBarProps; + + return ( + + + + + + ); +} + +storiesOf('SearchBar', module) + .add('default', () => wrapSearchBarInContext({ showQueryInput: true } as SearchBarProps)) + .add('with dataviewPicker', () => + wrapSearchBarInContext({ + dataViewPickerComponentProps: { + currentDataViewId: '1234', + trigger: { + 'data-test-subj': 'dataView-switch-link', + label: 'logstash-*', + title: 'logstash-*', + }, + onChangeDataView: action('onChangeDataView'), + }, + } as SearchBarProps) + ) + .add('with dataviewPicker enhanced', () => + wrapSearchBarInContext({ + dataViewPickerComponentProps: { + currentDataViewId: '1234', + trigger: { + 'data-test-subj': 'dataView-switch-link', + label: 'logstash-*', + title: 'logstash-*', + }, + onChangeDataView: action('onChangeDataView'), + onAddField: action('onAddField'), + onDataViewCreated: action('onDataViewCreated'), + }, + } as SearchBarProps) + ) + .add('with filterBar off', () => + wrapSearchBarInContext({ + showFilterBar: false, + } as SearchBarProps) + ) + .add('with query input off', () => + wrapSearchBarInContext({ + showQueryInput: false, + } as SearchBarProps) + ) + .add('with date picker off', () => + wrapSearchBarInContext({ + showDatePicker: false, + } as SearchBarProps) + ) + .add('with date picker off', () => + wrapSearchBarInContext({ + showDatePicker: false, + } as SearchBarProps) + ) + .add('with only the date picker on', () => + wrapSearchBarInContext({ + showDatePicker: true, + showFilterBar: false, + showQueryInput: false, + } as SearchBarProps) + ) + .add('with only the filter bar on', () => + wrapSearchBarInContext({ + showDatePicker: false, + showFilterBar: true, + showQueryInput: false, + filters: [ + { + meta: { + index: '1234', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + } as unknown as SearchBarProps) + ) + .add('with only the query bar on', () => + wrapSearchBarInContext({ + showDatePicker: false, + showFilterBar: false, + showQueryInput: true, + query: { query: 'Test: miaou', language: 'kuery' }, + } as unknown as SearchBarProps) + ) + .add('with only the filter bar and the date picker on', () => + wrapSearchBarInContext({ + showDatePicker: true, + showFilterBar: true, + showQueryInput: false, + filters: [ + { + meta: { + index: '1234', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + } as unknown as SearchBarProps) + ) + .add('with loaded saved query without changes', () => + wrapSearchBarInContext({ + savedQuery: { + id: '0173d0d0-b19a-11ec-8323-837d6b231b82', + attributes: { + title: 'test', + description: '', + query: { + query: '', + language: 'kuery', + }, + filters: [ + { + meta: { + index: '1234', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }, + }, + filters: [ + { + meta: { + index: '1234', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + } as unknown as SearchBarProps) + ) + .add('with loaded saved query with changes', () => + wrapSearchBarInContext({ + savedQuery: { + id: '0173d0d0-b19a-11ec-8323-837d6b231b82', + attributes: { + title: 'test', + description: '', + query: { + query: '', + language: 'kuery', + }, + filters: [ + { + meta: { + index: '1234', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ], + }, + }, + } as unknown as SearchBarProps) + ) + .add('show only query bar without submit', () => + wrapSearchBarInContext({ + showDatePicker: false, + showFilterBar: false, + showAutoRefreshOnly: false, + showQueryInput: true, + showSubmitButton: false, + } as SearchBarProps) + ); diff --git a/test/functional/config.coverage.js b/src/plugins/unified_search/public/dataview_picker/change_dataview.styles.ts similarity index 56% rename from test/functional/config.coverage.js rename to src/plugins/unified_search/public/dataview_picker/change_dataview.styles.ts index 8b0a59ac88b1b..1c505752d392c 100644 --- a/test/functional/config.coverage.js +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.styles.ts @@ -6,18 +6,15 @@ * Side Public License, v 1. */ -export default async function ({ readConfigFile }) { - const defaultConfig = await readConfigFile(require.resolve('./config')); +export const DATA_VIEW_POPOVER_CONTENT_WIDTH = 280; +export const changeDataViewStyles = ({ fullWidth }: { fullWidth?: boolean }) => { return { - ...defaultConfig.getAll(), - - suiteTags: { - exclude: ['skipCoverage'], + trigger: { + maxWidth: fullWidth ? undefined : DATA_VIEW_POPOVER_CONTENT_WIDTH, }, - - junit: { - reportName: 'Code Coverage for Functional Tests', + popoverContent: { + width: DATA_VIEW_POPOVER_CONTENT_WIDTH, }, }; -} +}; diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx b/src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx new file mode 100644 index 0000000000000..d3081561a0c4e --- /dev/null +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { I18nProvider } from '@kbn/i18n-react'; +import { act } from 'react-dom/test-utils'; +import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { ChangeDataView } from './change_dataview'; +import { EuiTourStep } from '@elastic/eui'; +import type { DataViewPickerProps } from '.'; + +describe('DataView component', () => { + const createMockWebStorage = () => ({ + clear: jest.fn(), + getItem: jest.fn(), + key: jest.fn(), + removeItem: jest.fn(), + setItem: jest.fn(), + length: 0, + }); + + const createMockStorage = () => ({ + storage: createMockWebStorage(), + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + }); + const getStorage = (v: boolean) => { + const storage = createMockStorage(); + storage.get.mockReturnValue(v); + return storage; + }; + + function wrapDataViewComponentInContext(testProps: DataViewPickerProps, storageValue: boolean) { + let dataMock = dataPluginMock.createStartContract(); + dataMock = { + ...dataMock, + dataViews: { + ...dataMock.dataViews, + getIdsWithTitle: jest.fn(), + }, + }; + const services = { + data: dataMock, + storage: getStorage(storageValue), + }; + + return ( + + + + + + ); + } + let props: DataViewPickerProps; + beforeEach(() => { + props = { + currentDataViewId: 'dataview-1', + trigger: { + label: 'Dataview 1', + title: 'Dataview 1', + fullWidth: true, + 'data-test-subj': 'dataview-trigger', + }, + onChangeDataView: jest.fn(), + }; + }); + it('should not render the tour component by default', async () => { + await act(async () => { + const component = mount(wrapDataViewComponentInContext(props, true)); + expect(component.find(EuiTourStep).prop('isStepOpen')).toBe(false); + }); + }); + it('should render the tour component if the showNewMenuTour is true', async () => { + const component = mount( + wrapDataViewComponentInContext({ ...props, showNewMenuTour: true }, false) + ); + expect(component.find(EuiTourStep).prop('isStepOpen')).toBe(true); + }); + + it('should not render the add runtime field menu if addField is not given', async () => { + await act(async () => { + const component = mount(wrapDataViewComponentInContext(props, true)); + findTestSubject(component, 'dataview-trigger').simulate('click'); + expect(component.find('[data-test-subj="indexPattern-add-field"]').length).toBe(0); + }); + }); + + it('should render the add runtime field menu if addField is given', async () => { + const addFieldSpy = jest.fn(); + const component = mount( + wrapDataViewComponentInContext( + { ...props, onAddField: addFieldSpy, showNewMenuTour: true }, + false + ) + ); + findTestSubject(component, 'dataview-trigger').simulate('click'); + expect(component.find('[data-test-subj="indexPattern-add-field"]').at(0).text()).toContain( + 'Add a field to this data view' + ); + component.find('[data-test-subj="indexPattern-add-field"]').first().simulate('click'); + expect(addFieldSpy).toHaveBeenCalled(); + }); + + it('should not render the add datavuew menu if onDataViewCreated is not given', async () => { + await act(async () => { + const component = mount(wrapDataViewComponentInContext(props, true)); + findTestSubject(component, 'dataview-trigger').simulate('click'); + expect(component.find('[data-test-subj="idataview-create-new"]').length).toBe(0); + }); + }); + + it('should render the add datavuew menu if onDataViewCreated is given', async () => { + const addDataViewSpy = jest.fn(); + const component = mount( + wrapDataViewComponentInContext( + { ...props, onDataViewCreated: addDataViewSpy, showNewMenuTour: true }, + false + ) + ); + findTestSubject(component, 'dataview-trigger').simulate('click'); + expect(component.find('[data-test-subj="dataview-create-new"]').at(0).text()).toContain( + 'Create a data view' + ); + component.find('[data-test-subj="dataview-create-new"]').first().simulate('click'); + expect(addDataViewSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx new file mode 100644 index 0000000000000..3e0ed7cc8a266 --- /dev/null +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx @@ -0,0 +1,244 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useState, useEffect } from 'react'; +import { css } from '@emotion/react'; +import { + EuiPopover, + EuiHorizontalRule, + EuiButton, + EuiContextMenuPanel, + EuiContextMenuItem, + useEuiTheme, + useGeneratedHtmlId, + EuiIcon, + EuiLink, + EuiText, + EuiTourStep, + EuiContextMenuPanelProps, +} from '@elastic/eui'; +import type { DataViewListItem } from '@kbn/data-views-plugin/public'; +import { IDataPluginServices } from '@kbn/data-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { DataViewPickerProps } from '.'; +import { DataViewsList } from './dataview_list'; +import { changeDataViewStyles } from './change_dataview.styles'; + +const NEW_DATA_VIEW_MENU_STORAGE_KEY = 'data.newDataViewMenu'; + +const newMenuTourTitle = i18n.translate('unifiedSearch.query.dataViewMenu.newMenuTour.title', { + defaultMessage: 'A better data view menu', +}); + +const newMenuTourDescription = i18n.translate( + 'unifiedSearch.query.dataViewMenu.newMenuTour.description', + { + defaultMessage: + 'This menu now offers all the tools you need to create, find, and edit your data views.', + } +); + +const newMenuTourDismissLabel = i18n.translate( + 'unifiedSearch.query.dataViewMenu.newMenuTour.dismissLabel', + { + defaultMessage: 'Got it', + } +); + +export function ChangeDataView({ + isMissingCurrent, + currentDataViewId, + onChangeDataView, + onAddField, + onDataViewCreated, + trigger, + selectableProps, + showNewMenuTour = false, +}: DataViewPickerProps) { + const { euiTheme } = useEuiTheme(); + const [isPopoverOpen, setPopoverIsOpen] = useState(false); + const [dataViewsList, setDataViewsList] = useState([]); + const [triggerLabel, setTriggerLabel] = useState(''); + const kibana = useKibana(); + const { application, data, storage } = kibana.services; + const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth }); + + const [isTourDismissed, setIsTourDismissed] = useState(() => + Boolean(storage.get(NEW_DATA_VIEW_MENU_STORAGE_KEY)) + ); + const [isTourOpen, setIsTourOpen] = useState(false); + + useEffect(() => { + if (showNewMenuTour && !isTourDismissed) { + setIsTourOpen(true); + } + }, [isTourDismissed, setIsTourOpen, showNewMenuTour]); + + const onTourDismiss = () => { + storage.set(NEW_DATA_VIEW_MENU_STORAGE_KEY, true); + setIsTourDismissed(true); + setIsTourOpen(false); + }; + + // Create a reusable id to ensure search input is the first focused item in the popover even though it's not the first item + const searchListInputId = useGeneratedHtmlId({ prefix: 'dataviewPickerListSearchInput' }); + + useEffect(() => { + const fetchDataViews = async () => { + const dataViewsRefs = await data.dataViews.getIdsWithTitle(); + setDataViewsList(dataViewsRefs); + }; + fetchDataViews(); + }, [data, currentDataViewId]); + + useEffect(() => { + if (trigger.label) { + setTriggerLabel(trigger.label); + } + }, [trigger.label]); + + const createTrigger = function () { + const { label, title, 'data-test-subj': dataTestSubj, fullWidth, ...rest } = trigger; + return ( + { + setPopoverIsOpen(!isPopoverOpen); + setIsTourOpen(false); + // onTourDismiss(); TODO: Decide if opening the menu should also dismiss the tour + }} + color={isMissingCurrent ? 'danger' : 'primary'} + iconSide="right" + iconType="arrowDown" + title={title} + fullWidth={fullWidth} + {...rest} + > + {triggerLabel} + + ); + }; + + const getPanelItems = () => { + const panelItems: EuiContextMenuPanelProps['items'] = []; + if (onAddField) { + panelItems.push( + { + setPopoverIsOpen(false); + onAddField(); + }} + > + {i18n.translate('unifiedSearch.query.queryBar.indexPattern.addFieldButton', { + defaultMessage: 'Add a field to this data view', + })} + , + { + setPopoverIsOpen(false); + application.navigateToApp('management', { + path: `/kibana/indexPatterns/patterns/${currentDataViewId}`, + }); + }} + > + {i18n.translate('unifiedSearch.query.queryBar.indexPattern.manageFieldButton', { + defaultMessage: 'Manage this data view', + })} + , + + ); + } + panelItems.push( + { + onChangeDataView(newId); + setPopoverIsOpen(false); + }} + currentDataViewId={currentDataViewId} + selectableProps={selectableProps} + searchListInputId={searchListInputId} + /> + ); + + if (onDataViewCreated) { + panelItems.push( + , + { + setPopoverIsOpen(false); + onDataViewCreated(); + }} + > + {i18n.translate('unifiedSearch.query.queryBar.indexPattern.addNewDataView', { + defaultMessage: 'Create a data view', + })} + + ); + } + + return panelItems; + }; + + return ( + +   {newMenuTourTitle} + + } + content={ + +

{newMenuTourDescription}

+
+ } + isStepOpen={isTourOpen} + onFinish={onTourDismiss} + step={1} + stepsTotal={1} + footerAction={ + + {newMenuTourDismissLabel} + + } + repositionOnScroll + display="block" + > + setPopoverIsOpen(false)} + panelPaddingSize="none" + initialFocus={`#${searchListInputId}`} + display="block" + buffer={8} + > +
+ +
+
+
+ ); +} diff --git a/src/plugins/unified_search/public/dataview_picker/dataview_list.test.tsx b/src/plugins/unified_search/public/dataview_picker/dataview_list.test.tsx new file mode 100644 index 0000000000000..813beae20369c --- /dev/null +++ b/src/plugins/unified_search/public/dataview_picker/dataview_list.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiSelectable } from '@elastic/eui'; +import { act } from 'react-dom/test-utils'; +import { ShallowWrapper } from 'enzyme'; +import { shallowWithIntl as shallow } from '@kbn/test-jest-helpers'; +import { DataViewsList, DataViewsListProps } from './dataview_list'; + +function getDataViewPickerList(instance: ShallowWrapper) { + return instance.find(EuiSelectable).first(); +} + +function getDataViewPickerOptions(instance: ShallowWrapper) { + return getDataViewPickerList(instance).prop('options'); +} + +function selectDataViewPickerOption(instance: ShallowWrapper, selectedLabel: string) { + const options: Array<{ label: string; checked?: 'on' | 'off' }> = getDataViewPickerOptions( + instance + ).map((option: { label: string }) => + option.label === selectedLabel + ? { ...option, checked: 'on' } + : { ...option, checked: undefined } + ); + return getDataViewPickerList(instance).prop('onChange')!(options); +} + +describe('DataView list component', () => { + const list = [ + { + id: 'dataview-1', + title: 'dataview-1', + }, + { + id: 'dataview-2', + title: 'dataview-2', + }, + ]; + const changeDataViewSpy = jest.fn(); + let props: DataViewsListProps; + beforeEach(() => { + props = { + currentDataViewId: 'dataview-1', + onChangeDataView: changeDataViewSpy, + dataViewsList: list, + }; + }); + it('should trigger the onChangeDataView if a new dataview is selected', async () => { + const component = shallow(); + await act(async () => { + selectDataViewPickerOption(component, 'dataview-2'); + }); + expect(changeDataViewSpy).toHaveBeenCalled(); + }); + + it('should list all dataviiew', () => { + const component = shallow(); + + expect(getDataViewPickerOptions(component)!.map((option: any) => option.label)).toEqual([ + 'dataview-1', + 'dataview-2', + ]); + }); +}); diff --git a/src/plugins/unified_search/public/dataview_picker/dataview_list.tsx b/src/plugins/unified_search/public/dataview_picker/dataview_list.tsx new file mode 100644 index 0000000000000..153cbdd3cf3f2 --- /dev/null +++ b/src/plugins/unified_search/public/dataview_picker/dataview_list.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiSelectable, EuiSelectableProps, EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import { DataViewListItem } from '@kbn/data-views-plugin/public'; + +export interface DataViewsListProps { + dataViewsList: DataViewListItem[]; + onChangeDataView: (newId: string) => void; + currentDataViewId?: string; + selectableProps?: EuiSelectableProps; + searchListInputId?: string; +} + +export function DataViewsList({ + dataViewsList, + onChangeDataView, + currentDataViewId, + selectableProps, + searchListInputId, +}: DataViewsListProps) { + return ( + + {...selectableProps} + data-test-subj="indexPattern-switcher" + searchable + singleSelection="always" + options={dataViewsList?.map(({ title, id }) => ({ + key: id, + label: title, + value: id, + checked: id === currentDataViewId ? 'on' : undefined, + }))} + onChange={(choices) => { + const choice = choices.find(({ checked }) => checked) as unknown as { + value: string; + }; + onChangeDataView(choice.value); + }} + searchProps={{ + id: searchListInputId, + compressed: true, + placeholder: i18n.translate('unifiedSearch.query.queryBar.indexPattern.findDataView', { + defaultMessage: 'Find a data view', + }), + ...(selectableProps ? selectableProps.searchProps : undefined), + }} + > + {(list, search) => ( + + {search} + {list} + + )} + + ); +} diff --git a/src/plugins/unified_search/public/dataview_picker/index.tsx b/src/plugins/unified_search/public/dataview_picker/index.tsx new file mode 100644 index 0000000000000..bd24aef0498ef --- /dev/null +++ b/src/plugins/unified_search/public/dataview_picker/index.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import type { EuiButtonProps, EuiSelectableProps } from '@elastic/eui'; +import { ChangeDataView } from './change_dataview'; + +export type ChangeDataViewTriggerProps = EuiButtonProps & { + label: string; + title?: string; +}; + +/** @public */ +export interface DataViewPickerProps { + trigger: ChangeDataViewTriggerProps; + isMissingCurrent?: boolean; + onChangeDataView: (newId: string) => void; + currentDataViewId?: string; + selectableProps?: EuiSelectableProps; + onAddField?: () => void; + onDataViewCreated?: () => void; + showNewMenuTour?: boolean; +} + +export const DataViewPicker = ({ + isMissingCurrent, + currentDataViewId, + onChangeDataView, + onAddField, + onDataViewCreated, + trigger, + selectableProps, + showNewMenuTour, +}: DataViewPickerProps) => { + return ( + + ); +}; diff --git a/src/plugins/unified_search/public/filter_bar/_global_filter_group.scss b/src/plugins/unified_search/public/filter_bar/_global_filter_group.scss deleted file mode 100644 index 24f3ca05a5685..0000000000000 --- a/src/plugins/unified_search/public/filter_bar/_global_filter_group.scss +++ /dev/null @@ -1,54 +0,0 @@ -// SASSTODO: Probably not the right file for this selector, but temporary until the files get re-organized -.globalQueryBar { - padding: 0 $euiSizeS $euiSizeS $euiSizeS; -} - -.globalQueryBar:first-child { - padding-top: $euiSizeS; -} - -.globalQueryBar:not(:empty) { - padding-bottom: $euiSizeS; -} - -.globalQueryBar--inPage { - padding: 0; -} - -.globalFilterGroup__filterBar { - margin-top: $euiSizeXS; -} - -.globalFilterBar__addButton { - min-height: $euiSizeL + $euiSizeXS; // same height as the badges -} - -// sass-lint:disable quotes -.globalFilterGroup__branch { - padding: $euiSizeS $euiSizeM 0 0; - background-repeat: no-repeat; - background-position: $euiSizeM ($euiSizeS * -1); - background-image: url("data:image/svg+xml,%0A%3Csvg width='28px' height='28px' viewBox='0 0 28 28' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='#{hexToRGB($euiColorLightShade)}'%3E%3Crect x='14' y='27' width='14' height='1'%3E%3C/rect%3E%3C/g%3E%3C/svg%3E"); -} - -.globalFilterGroup__wrapper { - line-height: 1; // Override kuiLocalNav & kuiLocalNavRow - overflow: hidden; - transition: height $euiAnimSpeedNormal $euiAnimSlightResistance; -} - -.globalFilterGroup__filterFlexItem { - overflow: hidden; - padding-bottom: 2px; // Allow the shadows of the pills to show -} - -.globalFilterBar__flexItem { - max-width: calc(100% - #{$euiSizeXS}); // Width minus margin around each flex itm -} - -@include euiBreakpoint('xs', 's') { - .globalFilterGroup__wrapper-isVisible { - // EUI Flexbox adds too much margin between responded items, this just moves it up - margin-top: $euiSize * -1; - } -} diff --git a/src/plugins/unified_search/public/filter_bar/_index.scss b/src/plugins/unified_search/public/filter_bar/_index.scss deleted file mode 100644 index 5333aff8b87da..0000000000000 --- a/src/plugins/unified_search/public/filter_bar/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'variables'; -@import 'global_filter_group'; -@import 'global_filter_item'; diff --git a/src/plugins/unified_search/public/filter_bar/filter_bar.styles.ts b/src/plugins/unified_search/public/filter_bar/filter_bar.styles.ts new file mode 100644 index 0000000000000..919655e0af160 --- /dev/null +++ b/src/plugins/unified_search/public/filter_bar/filter_bar.styles.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UseEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; + +export const filterBarStyles = ({ euiTheme }: UseEuiTheme, afterQueryBar?: boolean) => { + return { + group: css` + gap: ${euiTheme.size.xs}; + + &:not(:empty) { + margin-top: ${afterQueryBar ? euiTheme.size.s : 0}; + } + `, + }; +}; diff --git a/src/plugins/unified_search/public/filter_bar/filter_bar.tsx b/src/plugins/unified_search/public/filter_bar/filter_bar.tsx index 43b511b2c9f7d..ec7205d3a7df7 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_bar.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_bar.tsx @@ -6,224 +6,49 @@ * Side Public License, v 1. */ -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n-react'; -import { - buildEmptyFilter, - Filter, - enableFilter, - disableFilter, - pinFilter, - toggleFilterDisabled, - toggleFilterNegated, - unpinFilter, -} from '@kbn/es-query'; -import classNames from 'classnames'; -import React, { useState, useRef } from 'react'; - -import { METRIC_TYPE } from '@kbn/analytics'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { UI_SETTINGS } from '@kbn/data-plugin/common'; -import { IDataPluginServices } from '@kbn/data-plugin/public'; +import { EuiFlexGroup, useEuiTheme } from '@elastic/eui'; +import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; +import type { Filter } from '@kbn/es-query'; +import React, { useRef } from 'react'; import { DataView } from '@kbn/data-views-plugin/public'; -import { FilterOptions } from './filter_options'; -import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item'; -import { FilterEditor } from './filter_editor'; +import FilterItems from './filter_item/filter_items'; + +import { filterBarStyles } from './filter_bar.styles'; export interface Props { filters: Filter[]; onFiltersUpdated?: (filters: Filter[]) => void; - className: string; + className?: string; indexPatterns: DataView[]; intl: InjectedIntl; - appName: string; timeRangeForSuggestionsOverride?: boolean; + /** + * Applies extra styles necessary when coupled with the query bar + */ + afterQueryBar?: boolean; } const FilterBarUI = React.memo(function FilterBarUI(props: Props) { + const euiTheme = useEuiTheme(); + const styles = filterBarStyles(euiTheme, props.afterQueryBar); const groupRef = useRef(null); - const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false); - const kibana = useKibana(); - const { appName, usageCollection, uiSettings } = kibana.services; - if (!uiSettings) return null; - - const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); - - function onFiltersUpdated(filters: Filter[]) { - if (props.onFiltersUpdated) { - props.onFiltersUpdated(filters); - } - } - - const onAddFilterClick = () => setIsAddFilterPopoverOpen(!isAddFilterPopoverOpen); - - function renderItems() { - return props.filters.map((filter, i) => ( - - onUpdate(i, newFilter)} - onRemove={() => onRemove(i)} - indexPatterns={props.indexPatterns} - uiSettings={uiSettings!} - timeRangeForSuggestionsOverride={props.timeRangeForSuggestionsOverride} - /> - - )); - } - - function renderAddFilter() { - const isPinned = uiSettings!.get(UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT); - const [indexPattern] = props.indexPatterns; - const index = indexPattern && indexPattern.id; - const newFilter = buildEmptyFilter(isPinned, index); - - const button = ( - - +{' '} - - - ); - - return ( - - setIsAddFilterPopoverOpen(false)} - anchorPosition="downLeft" - panelPaddingSize="none" - initialFocus=".filterEditor__hiddenItem" - ownFocus - repositionOnScroll - > - -
- setIsAddFilterPopoverOpen(false)} - key={JSON.stringify(newFilter)} - timeRangeForSuggestionsOverride={props.timeRangeForSuggestionsOverride} - /> -
-
-
-
- ); - } - - function onAdd(filter: Filter) { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:added`); - setIsAddFilterPopoverOpen(false); - - const filters = [...props.filters, filter]; - onFiltersUpdated(filters); - } - - function onRemove(i: number) { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:removed`); - const filters = [...props.filters]; - filters.splice(i, 1); - onFiltersUpdated(filters); - groupRef.current?.focus(); - } - - function onUpdate(i: number, filter: Filter) { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:edited`); - const filters = [...props.filters]; - filters[i] = filter; - onFiltersUpdated(filters); - } - - function onEnableAll() { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:enable_all`); - const filters = props.filters.map(enableFilter); - onFiltersUpdated(filters); - } - - function onDisableAll() { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:disable_all`); - const filters = props.filters.map(disableFilter); - onFiltersUpdated(filters); - } - - function onPinAll() { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:pin_all`); - const filters = props.filters.map(pinFilter); - onFiltersUpdated(filters); - } - - function onUnpinAll() { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:unpin_all`); - const filters = props.filters.map(unpinFilter); - onFiltersUpdated(filters); - } - - function onToggleAllNegated() { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:invert_all`); - const filters = props.filters.map(toggleFilterNegated); - onFiltersUpdated(filters); - } - - function onToggleAllDisabled() { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:toggle_all`); - const filters = props.filters.map(toggleFilterDisabled); - onFiltersUpdated(filters); - } - - function onRemoveAll() { - reportUiCounter?.(METRIC_TYPE.CLICK, `filter:remove_all`); - onFiltersUpdated([]); - } - - const classes = classNames('globalFilterBar', props.className); return ( - - - - - - - {renderItems()} - {renderAddFilter()} - - + ); }); diff --git a/src/plugins/unified_search/public/filter_bar/filter_button_group/filter_button_group.scss b/src/plugins/unified_search/public/filter_bar/filter_button_group/filter_button_group.scss new file mode 100644 index 0000000000000..95b87e1d827c6 --- /dev/null +++ b/src/plugins/unified_search/public/filter_bar/filter_button_group/filter_button_group.scss @@ -0,0 +1,24 @@ +.kbnFilterButtonGroup { + height: $euiFormControlHeight; + background-color: $euiFormInputGroupLabelBackground; + border-radius: $euiFormControlBorderRadius; + box-shadow: 0 0 1px inset rgba($euiFormBorderOpaqueColor, .4); + + // Targets any interactable elements + *:enabled { + transform: none !important; + } + + &--s { + height: $euiFormControlCompressedHeight; + } + + &--attached { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + > *:not(:last-of-type) { + border-right: 1px solid $euiFormBorderColor; + } +} diff --git a/src/plugins/unified_search/public/filter_bar/filter_button_group/filter_button_group.tsx b/src/plugins/unified_search/public/filter_bar/filter_button_group/filter_button_group.tsx new file mode 100644 index 0000000000000..1de5c71f4a301 --- /dev/null +++ b/src/plugins/unified_search/public/filter_bar/filter_button_group/filter_button_group.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 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 './filter_button_group.scss'; + +import React, { FC, ReactNode } from 'react'; +import classNames from 'classnames'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +interface Props { + items: ReactNode[]; + /** + * Displays the last item without a border radius as if attached to the next DOM node + */ + attached?: boolean; + /** + * Matches overall height with standard form/button sizes + */ + size?: 'm' | 's'; +} + +export const FilterButtonGroup: FC = ({ items, attached, size = 'm', ...rest }: Props) => { + return ( + + {items.map((item, i) => + item == null ? undefined : ( + + {item} + + ) + )} + + ); +}; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx index 972bf657723fc..ae7917e7a1c7a 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/index.tsx @@ -14,6 +14,7 @@ import { EuiFlexItem, EuiForm, EuiFormRow, + EuiPopoverFooter, EuiPopoverTitle, EuiSpacer, EuiSwitch, @@ -55,6 +56,7 @@ export interface Props { onCancel: () => void; intl: InjectedIntl; timeRangeForSuggestionsOverride?: boolean; + mode?: 'edit' | 'add'; } interface State { @@ -68,6 +70,20 @@ interface State { isCustomEditorOpen: boolean; } +const panelTitleAdd = i18n.translate('unifiedSearch.filter.filterEditor.addFilterPopupTitle', { + defaultMessage: 'Add filter', +}); +const panelTitleEdit = i18n.translate('unifiedSearch.filter.filterEditor.editFilterPopupTitle', { + defaultMessage: 'Edit filter', +}); + +const addButtonLabel = i18n.translate('unifiedSearch.filter.filterEditor.addButtonLabel', { + defaultMessage: 'Add filter', +}); +const updateButtonLabel = i18n.translate('unifiedSearch.filter.filterEditor.updateButtonLabel', { + defaultMessage: 'Update filter', +}); + class FilterEditorUI extends Component { constructor(props: Props) { super(props); @@ -86,14 +102,9 @@ class FilterEditorUI extends Component { public render() { return (
- + - - - + {this.props.mode === 'add' ? panelTitleAdd : panelTitleEdit} { -
- + +
{this.renderIndexPatternInput()} {this.state.isCustomEditorOpen ? this.renderCustomEditor() : this.renderRegularEditor()} @@ -154,9 +165,9 @@ class FilterEditorUI extends Component {
)} +
- - + { isDisabled={!this.isFilterValid()} data-test-subj="saveFilter" > - + {this.props.mode === 'add' ? addButtonLabel : updateButtonLabel} @@ -185,8 +193,8 @@ class FilterEditorUI extends Component { - -
+ +
); } @@ -207,32 +215,31 @@ class FilterEditorUI extends Component { } const { selectedIndexPattern } = this.state; return ( - - - + + - indexPattern.title} - onChange={this.onIndexPatternChange} - singleSelection={{ asPlainText: true }} - isClearable={false} - data-test-subj="filterIndexPatternsSelect" - /> - - - + options={this.props.indexPatterns} + selectedOptions={selectedIndexPattern ? [selectedIndexPattern] : []} + getLabel={(indexPattern) => indexPattern.title} + onChange={this.onIndexPatternChange} + singleSelection={{ asPlainText: true }} + isClearable={false} + data-test-subj="filterIndexPatternsSelect" + /> + + + ); } diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx index 3f2aaa50af0fc..601cf68141c49 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/lib/filter_label.tsx @@ -11,7 +11,7 @@ import { EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Filter, FILTERS } from '@kbn/data-plugin/common'; import { existsOperator, isOneOfOperator } from './filter_operators'; -import type { FilterLabelStatus } from '../../filter_item'; +import type { FilterLabelStatus } from '../../filter_item/filter_item'; export interface FilterLabelProps { filter: Filter; diff --git a/src/plugins/unified_search/public/filter_bar/_variables.scss b/src/plugins/unified_search/public/filter_bar/filter_item/_variables.scss similarity index 100% rename from src/plugins/unified_search/public/filter_bar/_variables.scss rename to src/plugins/unified_search/public/filter_bar/filter_item/_variables.scss diff --git a/src/plugins/unified_search/public/filter_bar/_global_filter_item.scss b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.scss similarity index 82% rename from src/plugins/unified_search/public/filter_bar/_global_filter_item.scss rename to src/plugins/unified_search/public/filter_bar/filter_item/filter_item.scss index 1c9cea7291770..94f64bdce2f65 100644 --- a/src/plugins/unified_search/public/filter_bar/_global_filter_item.scss +++ b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.scss @@ -1,3 +1,5 @@ +@import './variables'; + /** * 1. Allow wrapping of long filter items */ @@ -6,26 +8,14 @@ line-height: $euiSize; border: none; color: $euiTextColor; - padding-top: $euiSizeM / 2; - padding-bottom: $euiSizeM / 2; + padding-top: $euiSizeM / 2 + 1px; + padding-bottom: $euiSizeM / 2 + 1px; white-space: normal; /* 1 */ - .euiBadge__childButton { - flex-shrink: 1; /* 1 */ - } - - .euiBadge__iconButton:focus { - background-color: transparentize($euiColorPrimary, .9); - } - &:not(.globalFilterItem-isDisabled) { @include euiFormControlDefaultShadow; box-shadow: #{$euiFormControlBoxShadow}, inset 0 0 0 1px $kbnGlobalFilterItemBorderColor; // Make the actual border more visible } - - &:focus-within { - animation: none !important; // Remove focus ring animation otherwise it overrides simulated border via box-shadow - } } .globalFilterItem-isDisabled { @@ -81,7 +71,7 @@ } .globalFilterItem__editorForm { - padding: $euiSizeM; + padding: $euiSizeS; } .globalFilterItem__popover, diff --git a/src/plugins/unified_search/public/filter_bar/filter_item.tsx b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx similarity index 97% rename from src/plugins/unified_search/public/filter_bar/filter_item.tsx rename to src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx index 16234a2953dc7..6b06461b4f297 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_item.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_item/filter_item.tsx @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import './filter_item.scss'; + import { EuiContextMenu, EuiPopover, EuiPopoverProps } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n-react'; import { @@ -24,9 +26,9 @@ import { getDisplayValueFromFilter, getFieldDisplayValueFromFilter, } from '@kbn/data-plugin/public'; -import { FilterEditor } from './filter_editor'; -import { FilterView } from './filter_view'; -import { getIndexPatterns } from '../services'; +import { FilterEditor } from '../filter_editor'; +import { FilterView } from '../filter_view'; +import { getIndexPatterns } from '../../services'; type PanelOptions = 'pinFilter' | 'editFilter' | 'negateFilter' | 'disableFilter' | 'deleteFilter'; @@ -101,6 +103,11 @@ export function FilterItem(props: FilterItemProps) { } } + function handleIconClick(e: MouseEvent) { + props.onRemove(); + setIsPopoverOpen(false); + } + function onSubmit(f: Filter) { setIsPopoverOpen(false); props.onUpdate(f); @@ -363,7 +370,7 @@ export function FilterItem(props: FilterItemProps) { filterLabelStatus: valueLabelConfig.status, errorMessage: valueLabelConfig.message, className: getClasses(!!filter.meta.negate, valueLabelConfig), - iconOnClick: props.onRemove, + iconOnClick: handleIconClick, onClick: handleBadgeClick, 'data-test-subj': getDataTestSubj(valueLabelConfig), readonly: props.readonly, diff --git a/src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx b/src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx new file mode 100644 index 0000000000000..95d49450dd390 --- /dev/null +++ b/src/plugins/unified_search/public/filter_bar/filter_item/filter_items.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useRef } from 'react'; +import { css } from '@emotion/react'; +import { EuiFlexItem } from '@elastic/eui'; +import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; +import type { Filter } from '@kbn/es-query'; +import { IDataPluginServices } from '@kbn/data-plugin/public'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { DataView } from '@kbn/data-views-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { FilterItem } from './filter_item'; + +export interface Props { + filters: Filter[]; + onFiltersUpdated?: (filters: Filter[]) => void; + indexPatterns: DataView[]; + intl: InjectedIntl; + timeRangeForSuggestionsOverride?: boolean; +} + +const FilterItemsUI = React.memo(function FilterItemsUI(props: Props) { + const groupRef = useRef(null); + const kibana = useKibana(); + const { appName, usageCollection, uiSettings } = kibana.services; + if (!uiSettings) return null; + + const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); + + function onFiltersUpdated(filters: Filter[]) { + if (props.onFiltersUpdated) { + props.onFiltersUpdated(filters); + } + } + + function renderItems() { + return props.filters.map((filter, i) => ( + + onUpdate(i, newFilter)} + onRemove={() => onRemove(i)} + indexPatterns={props.indexPatterns} + uiSettings={uiSettings!} + timeRangeForSuggestionsOverride={props.timeRangeForSuggestionsOverride} + /> + + )); + } + + function onRemove(i: number) { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:removed`); + const filters = [...props.filters]; + filters.splice(i, 1); + onFiltersUpdated(filters); + groupRef.current?.focus(); + } + + function onUpdate(i: number, filter: Filter) { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:edited`); + const filters = [...props.filters]; + filters[i] = filter; + onFiltersUpdated(filters); + } + + return <>{renderItems()}; +}); + +const FilterItems = injectI18n(FilterItemsUI); +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default FilterItems; diff --git a/src/plugins/unified_search/public/filter_bar/filter_options.tsx b/src/plugins/unified_search/public/filter_bar/filter_options.tsx deleted file mode 100644 index d2e229c988711..0000000000000 --- a/src/plugins/unified_search/public/filter_bar/filter_options.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EuiButtonIcon, EuiContextMenu, EuiPopover, EuiPopoverTitle } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n-react'; -import { Component } from 'react'; -import React from 'react'; - -interface Props { - onEnableAll: () => void; - onDisableAll: () => void; - onPinAll: () => void; - onUnpinAll: () => void; - onToggleAllNegated: () => void; - onToggleAllDisabled: () => void; - onRemoveAll: () => void; - intl: InjectedIntl; -} - -interface State { - isPopoverOpen: boolean; -} - -class FilterOptionsUI extends Component { - private buttonRef = React.createRef(); - - public state: State = { - isPopoverOpen: false, - }; - - public togglePopover = () => { - this.setState((prevState) => ({ - isPopoverOpen: !prevState.isPopoverOpen, - })); - }; - - public closePopover = () => { - this.setState({ isPopoverOpen: false }); - this.buttonRef.current?.focus(); - }; - - public render() { - const panelTree = { - id: 0, - items: [ - { - name: this.props.intl.formatMessage({ - id: 'unifiedSearch.filter.options.enableAllFiltersButtonLabel', - defaultMessage: 'Enable all', - }), - icon: 'eye', - onClick: () => { - this.closePopover(); - this.props.onEnableAll(); - }, - 'data-test-subj': 'enableAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'unifiedSearch.filter.options.disableAllFiltersButtonLabel', - defaultMessage: 'Disable all', - }), - icon: 'eyeClosed', - onClick: () => { - this.closePopover(); - this.props.onDisableAll(); - }, - 'data-test-subj': 'disableAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'unifiedSearch.filter.options.pinAllFiltersButtonLabel', - defaultMessage: 'Pin all', - }), - icon: 'pin', - onClick: () => { - this.closePopover(); - this.props.onPinAll(); - }, - 'data-test-subj': 'pinAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'unifiedSearch.filter.options.unpinAllFiltersButtonLabel', - defaultMessage: 'Unpin all', - }), - icon: 'pin', - onClick: () => { - this.closePopover(); - this.props.onUnpinAll(); - }, - 'data-test-subj': 'unpinAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'unifiedSearch.filter.options.invertNegatedFiltersButtonLabel', - defaultMessage: 'Invert inclusion', - }), - icon: 'invert', - onClick: () => { - this.closePopover(); - this.props.onToggleAllNegated(); - }, - 'data-test-subj': 'invertInclusionAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'unifiedSearch.filter.options.invertDisabledFiltersButtonLabel', - defaultMessage: 'Invert enabled/disabled', - }), - icon: 'eye', - onClick: () => { - this.closePopover(); - this.props.onToggleAllDisabled(); - }, - 'data-test-subj': 'invertEnableDisableAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'unifiedSearch.filter.options.deleteAllFiltersButtonLabel', - defaultMessage: 'Remove all', - }), - icon: 'trash', - onClick: () => { - this.closePopover(); - this.props.onRemoveAll(); - }, - 'data-test-subj': 'removeAllFilters', - }, - ], - }; - - return ( - - } - anchorPosition="rightUp" - panelPaddingSize="none" - repositionOnScroll - > - - - - - - ); - } -} - -export const FilterOptions = injectI18n(FilterOptionsUI); diff --git a/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx b/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx index 29ff160d50db6..d399bb0025a10 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_view/index.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import React, { FC } from 'react'; import { Filter, isFilterPinned } from '@kbn/es-query'; import { FilterLabel } from '..'; -import type { FilterLabelStatus } from '../filter_item'; +import type { FilterLabelStatus } from '../filter_item/filter_item'; interface Props { filter: Filter; diff --git a/src/plugins/unified_search/public/filter_bar/index.tsx b/src/plugins/unified_search/public/filter_bar/index.tsx index 70a108f359790..30f94c3972ee1 100644 --- a/src/plugins/unified_search/public/filter_bar/index.tsx +++ b/src/plugins/unified_search/public/filter_bar/index.tsx @@ -17,6 +17,13 @@ export const FilterBar = (props: React.ComponentProps) => ); +const LazyFilterItems = React.lazy(() => import('./filter_item/filter_items')); +export const FilterItems = (props: React.ComponentProps) => ( + }> + + +); + const LazyFilterLabel = React.lazy(() => import('./filter_editor/lib/filter_label')); export const FilterLabel = (props: React.ComponentProps) => ( }> @@ -24,7 +31,7 @@ export const FilterLabel = (props: React.ComponentProps) ); -const LazyFilterItem = React.lazy(() => import('./filter_item')); +const LazyFilterItem = React.lazy(() => import('./filter_item/filter_item')); export const FilterItem = (props: React.ComponentProps) => ( }> diff --git a/src/plugins/unified_search/public/index.scss b/src/plugins/unified_search/public/index.scss index 7f7704c64e9b4..72e1c0c313f74 100755 --- a/src/plugins/unified_search/public/index.scss +++ b/src/plugins/unified_search/public/index.scss @@ -3,5 +3,3 @@ @import './saved_query_management/index'; @import './query_string_input/index'; - -@import './filter_bar/index'; diff --git a/src/plugins/unified_search/public/index.ts b/src/plugins/unified_search/public/index.ts index 93805c6cfec1c..bc7974b42efb3 100755 --- a/src/plugins/unified_search/public/index.ts +++ b/src/plugins/unified_search/public/index.ts @@ -15,6 +15,8 @@ export type { StatefulSearchBarProps, SearchBarProps } from './search_bar'; export type { UnifiedSearchPublicPluginStart, UnifiedSearchPluginSetup } from './types'; export { SearchBar } from './search_bar'; export { FilterLabel, FilterItem } from './filter_bar'; +export { DataViewsList } from './dataview_picker/dataview_list'; +export { DataViewPicker } from './dataview_picker'; export type { ApplyGlobalFilterActionContext } from './actions'; export { ACTION_GLOBAL_APPLY_FILTER } from './actions'; diff --git a/src/plugins/unified_search/public/plugin.ts b/src/plugins/unified_search/public/plugin.ts index 5ba2474066275..26727b56094a0 100755 --- a/src/plugins/unified_search/public/plugin.ts +++ b/src/plugins/unified_search/public/plugin.ts @@ -5,9 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import './index.scss'; - import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import { Storage, IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; diff --git a/src/plugins/unified_search/public/query_string_input/_query_bar.scss b/src/plugins/unified_search/public/query_string_input/_query_bar.scss index f8c2f067d9ec5..7b9a735d1556f 100644 --- a/src/plugins/unified_search/public/query_string_input/_query_bar.scss +++ b/src/plugins/unified_search/public/query_string_input/_query_bar.scss @@ -1,61 +1,34 @@ .kbnQueryBar__wrap { - max-width: 100%; + width: 100%; z-index: $euiZContentMenu; -} + height: $euiFormControlHeight; + display: flex; -// Uses the append style, but no bordering -.kqlQueryBar__languageSwitcherButton { - border-right: none !important; - border-left: $euiFormInputGroupBorder; + > [aria-expanded='true'] { + // Using filter allows it to adhere the children's bounds + filter: drop-shadow(0 5.7px 12px rgba($euiShadowColor, shadowOpacity(.05))); + } } .kbnQueryBar__textareaWrap { + position: relative; overflow: visible !important; // Override EUI form control display: flex; flex: 1 1 100%; - position: relative; - background-color: $euiFormBackgroundColor; - border-radius: $euiFormControlBorderRadius; - - &.kbnQueryBar__textareaWrap--hasPrepend { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - - &.kbnQueryBar__textareaWrap--hasAppend { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } } .kbnQueryBar__textarea { z-index: $euiZContentMenu; resize: none !important; // When in the group, it will autosize - height: $euiFormControlHeight - 2px; + height: $euiFormControlHeight; // Unlike most inputs within layout control groups, the text area still needs a border // for multi-line content. These adjusts help it sit above the control groups // shadow to line up correctly. - padding: $euiSizeS; - box-shadow: 0 0 0 1px $euiFormBorderColor; - padding-bottom: $euiSizeS + 1px; + padding: ($euiSizeS + 2px) $euiSizeS $euiSizeS; // Firefox adds margin to textarea margin: 0; - &.kbnQueryBar__textarea--hasPrepend { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - &.kbnQueryBar__textarea--hasAppend { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - &:not(.kbnQueryBar__textarea--autoHeight):not(:invalid) { - @include euiYScrollWithShadows; - } - &:not(.kbnQueryBar__textarea--autoHeight) { - white-space: nowrap; overflow-y: hidden; overflow-x: hidden; } @@ -65,18 +38,35 @@ overflow-x: auto; overflow-y: auto; white-space: normal; - box-shadow: 0 0 0 1px $euiFormBorderColor; + + } + + &.kbnQueryBar__textarea--isSuggestionsVisible { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + + &--isClearable { + @include euiFormControlWithIcon($isIconOptional: false, $side: 'right'); } @include euiFormControlWithIcon($isIconOptional: true); + ~ .euiFormControlLayoutIcons { // By default form control layout icon is vertically centered, but our textarea // can expand to be multi-line, so we position it with padding that matches // the parent textarea padding z-index: $euiZContentMenu + 1; - top: $euiSizeS + 3px; + top: $euiSizeM; bottom: unset; } + + &--withPrepend { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + margin-left: -1px; + width: calc(100% + 1px); + } } .kbnQueryBar__datePickerWrapper { @@ -92,32 +82,3 @@ } } } - -@include euiBreakpoint('xs', 's') { - .kbnQueryBar--withDatePicker { - > :first-child { - // Change the order of the query bar and date picker so that the date picker is top and the query bar still aligns with filters - order: 1; - // EUI Flexbox adds too much margin between responded items, this just moves it up - margin-top: $euiSizeS * -1; - } - } -} - -// IE specific fix for the datepicker to not collapse -@include euiBreakpoint('m', 'l', 'xl') { - .kbnQueryBar__datePickerWrapper { - max-width: 40vw; - // sass-lint:disable-block no-important - flex-grow: 0 !important; - flex-basis: auto !important; - - &.kbnQueryBar__datePickerWrapper-isHidden { - // sass-lint:disable-block no-important - margin-right: -$euiSizeXS !important; - width: 0; - overflow: hidden; - max-width: 0; - } - } -} diff --git a/src/plugins/unified_search/public/query_string_input/add_filter_popover.tsx b/src/plugins/unified_search/public/query_string_input/add_filter_popover.tsx new file mode 100644 index 0000000000000..b86d7d7f02498 --- /dev/null +++ b/src/plugins/unified_search/public/query_string_input/add_filter_popover.tsx @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexItem, + EuiButtonIcon, + EuiPopover, + EuiButtonIconProps, + EuiToolTip, +} from '@elastic/eui'; +import { Filter } from '@kbn/es-query'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { FilterEditorWrapper } from './filter_editor_wrapper'; + +interface AddFilterPopoverProps { + indexPatterns?: Array; + filters: Filter[]; + timeRangeForSuggestionsOverride?: boolean; + onFiltersUpdated?: (filters: Filter[]) => void; + buttonProps?: Partial; +} + +export const AddFilterPopover = React.memo(function AddFilterPopover({ + indexPatterns, + filters, + timeRangeForSuggestionsOverride, + onFiltersUpdated, + buttonProps, +}: AddFilterPopoverProps) { + const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false); + + const buttonIconLabel = i18n.translate('unifiedSearch.filter.filterBar.addFilterButtonLabel', { + defaultMessage: 'Add filter', + }); + + const button = ( + + setIsAddFilterPopoverOpen(!isAddFilterPopoverOpen)} + size="m" + {...buttonProps} + /> + + ); + + return ( + + setIsAddFilterPopoverOpen(false)} + anchorPosition="downLeft" + panelPaddingSize="none" + initialFocus=".filterEditor__hiddenItem" + ownFocus + repositionOnScroll + > + setIsAddFilterPopoverOpen(false)} + /> + + + ); +}); diff --git a/src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx b/src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx new file mode 100644 index 0000000000000..dd106607353f2 --- /dev/null +++ b/src/plugins/unified_search/public/query_string_input/filter_editor_wrapper.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useEffect } from 'react'; +import { Filter, buildEmptyFilter } from '@kbn/es-query'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { UI_SETTINGS } from '@kbn/data-plugin/common'; +import { IDataPluginServices } from '@kbn/data-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { FILTER_EDITOR_WIDTH } from '../filter_bar/filter_item/filter_item'; +import { FilterEditor } from '../filter_bar/filter_editor'; +import { fetchIndexPatterns } from './fetch_index_patterns'; + +interface FilterEditorWrapperProps { + indexPatterns?: Array; + filters: Filter[]; + timeRangeForSuggestionsOverride?: boolean; + closePopover?: () => void; + onFiltersUpdated?: (filters: Filter[]) => void; +} + +export const FilterEditorWrapper = React.memo(function FilterEditorWrapper({ + indexPatterns, + filters, + timeRangeForSuggestionsOverride, + closePopover, + onFiltersUpdated, +}: FilterEditorWrapperProps) { + const kibana = useKibana(); + const { uiSettings, data, usageCollection, appName } = kibana.services; + const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); + const [dataViews, setDataviews] = useState([]); + const [newFilter, setNewFilter] = useState(undefined); + const isPinned = uiSettings!.get(UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT); + + useEffect(() => { + const fetchDataViews = async () => { + const stringPatterns = indexPatterns?.filter( + (indexPattern) => typeof indexPattern === 'string' + ) as string[]; + const objectPatterns = indexPatterns?.filter( + (indexPattern) => typeof indexPattern !== 'string' + ) as DataView[]; + + const objectPatternsFromStrings = (await fetchIndexPatterns( + data.dataViews, + stringPatterns + )) as DataView[]; + setDataviews([...objectPatterns, ...objectPatternsFromStrings]); + const [dataView] = [...objectPatterns, ...objectPatternsFromStrings]; + const index = dataView && dataView.id; + const emptyFilter = buildEmptyFilter(isPinned, index); + setNewFilter(emptyFilter); + }; + if (indexPatterns) { + fetchDataViews(); + } + }, [data.dataViews, indexPatterns, isPinned]); + + function onAdd(filter: Filter) { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:added`); + closePopover?.(); + const updatedFilters = [...filters, filter]; + onFiltersUpdated?.(updatedFilters); + } + + return ( +
+ {newFilter && ( + closePopover?.()} + key={JSON.stringify(newFilter)} + timeRangeForSuggestionsOverride={timeRangeForSuggestionsOverride} + mode="add" + /> + )} +
+ ); +}); diff --git a/src/plugins/unified_search/public/query_string_input/language_switcher.test.tsx b/src/plugins/unified_search/public/query_string_input/language_switcher.test.tsx index 0223fc85a3ddb..591fe94360793 100644 --- a/src/plugins/unified_search/public/query_string_input/language_switcher.test.tsx +++ b/src/plugins/unified_search/public/query_string_input/language_switcher.test.tsx @@ -11,7 +11,7 @@ import { QueryLanguageSwitcher, QueryLanguageSwitcherProps } from './language_sw import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { coreMock } from '@kbn/core/public/mocks'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { EuiButtonEmpty, EuiIcon, EuiPopover } from '@elastic/eui'; +import { EuiButtonIcon, EuiIcon, EuiPopover } from '@elastic/eui'; const startMock = coreMock.createStart(); describe('LanguageSwitcher', () => { @@ -28,7 +28,7 @@ describe('LanguageSwitcher', () => { ); } - it('should toggle off if language is lucene', () => { + it('should select the lucene context menu if language is lucene', () => { const component = mountWithIntl( wrapInContext({ language: 'lucene', @@ -37,12 +37,14 @@ describe('LanguageSwitcher', () => { }, }) ); - component.find(EuiButtonEmpty).simulate('click'); + component.find(EuiButtonIcon).simulate('click'); expect(component.find(EuiPopover).prop('isOpen')).toBe(true); - expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeFalsy(); + expect(component.find('[data-test-subj="luceneLanguageMenuItem"]').get(0).props.icon).toBe( + 'check' + ); }); - it('should toggle on if language is kuery', () => { + it('should select the kql context menu if language is kuery', () => { const component = mountWithIntl( wrapInContext({ language: 'kuery', @@ -51,12 +53,14 @@ describe('LanguageSwitcher', () => { }, }) ); - component.find(EuiButtonEmpty).simulate('click'); + component.find(EuiButtonIcon).simulate('click'); expect(component.find(EuiPopover).prop('isOpen')).toBe(true); - expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeTruthy(); + expect(component.find('[data-test-subj="kqlLanguageMenuItem"]').get(0).props.icon).toBe( + 'check' + ); }); - it('should toggle off if language is text', () => { + it('should select the lucene context menu if language is text', () => { const component = mountWithIntl( wrapInContext({ language: 'text', @@ -65,9 +69,11 @@ describe('LanguageSwitcher', () => { }, }) ); - component.find(EuiButtonEmpty).simulate('click'); + component.find(EuiButtonIcon).simulate('click'); expect(component.find(EuiPopover).prop('isOpen')).toBe(true); - expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeFalsy(); + expect(component.find('[data-test-subj="luceneLanguageMenuItem"]').get(0).props.icon).toBe( + 'check' + ); }); it('it set language on nonKql mode text', () => { const onSelectLanguage = jest.fn(); @@ -79,11 +85,13 @@ describe('LanguageSwitcher', () => { onSelectLanguage, }) ); - component.find(EuiButtonEmpty).simulate('click'); + component.find(EuiButtonIcon).simulate('click'); expect(component.find(EuiPopover).prop('isOpen')).toBe(true); - expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeTruthy(); + expect(component.find('[data-test-subj="kqlLanguageMenuItem"]').get(0).props.icon).toBe( + 'check' + ); - component.find('[data-test-subj="languageToggle"]').at(1).simulate('click'); + component.find('[data-test-subj="luceneLanguageMenuItem"]').at(1).simulate('click'); expect(onSelectLanguage).toHaveBeenCalledWith('text'); }); @@ -97,8 +105,8 @@ describe('LanguageSwitcher', () => { onSelectLanguage, }) ); - component.find(EuiButtonEmpty).simulate('click'); - component.find('[data-test-subj="languageToggle"]').at(1).simulate('click'); + component.find(EuiButtonIcon).simulate('click'); + component.find('[data-test-subj="luceneLanguageMenuItem"]').at(1).simulate('click'); expect(onSelectLanguage).toHaveBeenCalledWith('lucene'); }); @@ -114,10 +122,10 @@ describe('LanguageSwitcher', () => { }) ); - expect(component.find(EuiIcon).prop('type')).toBe('boxesVertical'); + expect(component.find(EuiIcon).prop('type')).toBe('filter'); - component.find(EuiButtonEmpty).simulate('click'); - component.find('[data-test-subj="languageToggle"]').at(1).simulate('click'); + component.find(EuiButtonIcon).simulate('click'); + component.find('[data-test-subj="kqlLanguageMenuItem"]').at(1).simulate('click'); expect(onSelectLanguage).toHaveBeenCalledWith('kuery'); }); @@ -132,13 +140,12 @@ describe('LanguageSwitcher', () => { onSelectLanguage, }) ); - - expect(component.find('[data-test-subj="switchQueryLanguageButton"]').at(0).text()).toBe( - 'Lucene' + component.find(EuiButtonIcon).simulate('click'); + expect(component.find('[data-test-subj="luceneLanguageMenuItem"]').get(0).props.icon).toBe( + 'check' ); - component.find(EuiButtonEmpty).simulate('click'); - component.find('[data-test-subj="languageToggle"]').at(1).simulate('click'); + component.find('[data-test-subj="kqlLanguageMenuItem"]').at(1).simulate('click'); expect(onSelectLanguage).toHaveBeenCalledWith('kuery'); }); diff --git a/src/plugins/unified_search/public/query_string_input/language_switcher.tsx b/src/plugins/unified_search/public/query_string_input/language_switcher.tsx index db42339e464c3..a48901ef17f86 100644 --- a/src/plugins/unified_search/public/query_string_input/language_switcher.tsx +++ b/src/plugins/unified_search/public/query_string_input/language_switcher.tsx @@ -7,17 +7,13 @@ */ import { - EuiButtonEmpty, - EuiForm, - EuiFormRow, - EuiIcon, - EuiLink, EuiPopover, EuiPopoverTitle, - EuiSpacer, - EuiSwitch, - EuiText, PopoverAnchorPosition, + EuiContextMenuItem, + toSentenceCase, + EuiHorizontalRule, + EuiButtonIcon, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; @@ -29,7 +25,7 @@ export interface QueryLanguageSwitcherProps { onSelectLanguage: (newLanguage: string) => void; anchorPosition?: PopoverAnchorPosition; nonKqlMode?: 'lucene' | 'text'; - nonKqlModeHelpText?: string; + isOnTopBarMenu?: boolean; } export const QueryLanguageSwitcher = React.memo(function QueryLanguageSwitcher({ @@ -37,124 +33,78 @@ export const QueryLanguageSwitcher = React.memo(function QueryLanguageSwitcher({ anchorPosition, onSelectLanguage, nonKqlMode = 'lucene', - nonKqlModeHelpText, + isOnTopBarMenu, }: QueryLanguageSwitcherProps) { const kibana = useKibana(); const kueryQuerySyntaxDocs = kibana.services.docLinks!.links.query.kueryQuerySyntax; const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const luceneLabel = ( - - ); - const kqlLabel = ( - - ); - - const kqlFullName = ( - - ); - - const kqlModeTitle = i18n.translate('unifiedSearch.query.queryBar.languageSwitcher.toText', { - defaultMessage: 'Switch to Kibana Query Language for search', - }); const button = ( - setIsPopoverOpen(!isPopoverOpen)} className="euiFormControlLayout__append kqlQueryBar__languageSwitcherButton" data-test-subj={'switchQueryLanguageButton'} - > - {language === 'kuery' ? ( - kqlLabel - ) : nonKqlMode === 'lucene' ? ( - luceneLabel - ) : ( - - )} - + aria-label={i18n.translate('unifiedSearch.switchLanguage.buttonText', { + defaultMessage: 'Switch language button.', + })} + /> + ); + + const languageMenuItem = ( +
+ { + onSelectLanguage('kuery'); + }} + > + KQL + + { + onSelectLanguage(nonKqlMode); + }} + > + {toSentenceCase(nonKqlMode)} + + + + Documentation + +
); - return ( + const languageQueryStringComponent = ( setIsPopoverOpen(false)} repositionOnScroll - ownFocus={true} - initialFocus={'[role="switch"]'} + panelPaddingSize="none" > - + -
- -

- - {kqlFullName} - - ), - nonKqlModeHelpText: - nonKqlModeHelpText || - i18n.translate( - 'unifiedSearch.query.queryBar.syntaxOptionsDescription.nonKqlModeHelpText', - { - defaultMessage: 'Kibana uses Lucene.', - } - ), - }} - /> -

-
- - - - - - - ) : ( - - ) - } - checked={language === 'kuery'} - onChange={() => { - const newLanguage = language === 'kuery' ? nonKqlMode : 'kuery'; - onSelectLanguage(newLanguage); - }} - data-test-subj="languageToggle" - /> - - -
+ {languageMenuItem}
); + + return Boolean(isOnTopBarMenu) ? languageMenuItem : languageQueryStringComponent; }); diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx new file mode 100644 index 0000000000000..7a04b92c7e063 --- /dev/null +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx @@ -0,0 +1,278 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { I18nProvider } from '@kbn/i18n-react'; +import { act } from 'react-dom/test-utils'; +import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { coreMock } from '@kbn/core/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { Filter } from '@kbn/es-query'; +import { QueryBarMenuProps, QueryBarMenu } from './query_bar_menu'; +import { EuiPopover } from '@elastic/eui'; + +describe('Querybar Menu component', () => { + const createMockWebStorage = () => ({ + clear: jest.fn(), + getItem: jest.fn(), + key: jest.fn(), + removeItem: jest.fn(), + setItem: jest.fn(), + length: 0, + }); + + const createMockStorage = () => ({ + storage: createMockWebStorage(), + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + }); + const getStorage = (v: string) => { + const storage = createMockStorage(); + storage.get.mockReturnValue(v); + return storage; + }; + + const startMock = coreMock.createStart(); + let dataMock = dataPluginMock.createStartContract(); + function wrapQueryBarMenuComponentInContext(testProps: QueryBarMenuProps, storageValue: string) { + dataMock = { + ...dataMock, + dataViews: { + ...dataMock.dataViews, + getIdsWithTitle: jest.fn(), + }, + }; + const services = { + data: dataMock, + storage: getStorage(storageValue), + uiSettings: startMock.uiSettings, + }; + + return ( + + + + + + ); + } + let props: QueryBarMenuProps; + beforeEach(() => { + props = { + language: 'kuery', + onQueryChange: jest.fn(), + onQueryBarSubmit: jest.fn(), + toggleFilterBarMenuPopover: jest.fn(), + openQueryBarMenu: false, + savedQueryService: { + ...dataMock.query.savedQueries, + findSavedQueries: jest.fn().mockResolvedValue({ + queries: [ + { + id: '8a0b7cd0-b0c4-11ec-92b2-73d62e0d28a9', + attributes: { + title: 'Test', + description: '', + query: { + query: 'category.keyword : "Men\'s Shoes" ', + language: 'kuery', + }, + filters: [], + }, + }, + ], + }), + }, + }; + }); + it('should not render the popover if the openQueryBarMenu prop is false', async () => { + await act(async () => { + const component = mount(wrapQueryBarMenuComponentInContext(props, 'kuery')); + expect(component.find(EuiPopover).prop('isOpen')).toBe(false); + }); + }); + + it('should render the popover if the openQueryBarMenu prop is true', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + }; + await act(async () => { + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + expect(component.find(EuiPopover).prop('isOpen')).toBe(true); + }); + }); + + it('should render the context menu by default', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + }; + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + expect(component.find('[data-test-subj="queryBarMenuPanel"]')).toBeTruthy(); + }); + + it('should render the saved filter sets panels if the showQueryInput prop is true but disabled', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + showQueryInput: true, + showFilterBar: true, + }; + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + const saveFilterSetButton = component.find( + '[data-test-subj="saved-query-management-save-button"]' + ); + const loadFilterSetButton = component.find( + '[data-test-subj="saved-query-management-load-button"]' + ); + expect(saveFilterSetButton.length).toBeTruthy(); + expect(saveFilterSetButton.first().prop('disabled')).toBe(true); + expect(loadFilterSetButton.length).toBeTruthy(); + expect(loadFilterSetButton.first().prop('disabled')).toBe(true); + }); + + it('should render the filter sets panels if the showFilterBar is true but disabled', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + showFilterBar: true, + }; + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + const applyToAllFiltersButton = component.find( + '[data-test-subj="filter-sets-applyToAllFilters"]' + ); + const removeAllFiltersButton = component.find( + '[data-test-subj="filter-sets-removeAllFilters"]' + ); + expect(applyToAllFiltersButton.length).toBeTruthy(); + expect(applyToAllFiltersButton.first().prop('disabled')).toBe(true); + expect(removeAllFiltersButton.length).toBeTruthy(); + expect(removeAllFiltersButton.first().prop('disabled')).toBe(true); + }); + + it('should enable the clear all button if query is given', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + showFilterBar: true, + query: { + query: 'category.keyword : "Men\'s Shoes" ', + language: 'kuery', + }, + }; + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + const removeAllFiltersButton = component.find( + '[data-test-subj="filter-sets-removeAllFilters"]' + ); + expect(removeAllFiltersButton.first().prop('disabled')).toBe(false); + }); + + it('should enable the apply to all button if filter is given', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + showFilterBar: true, + filters: [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ] as Filter[], + }; + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + const applyToAllFiltersButton = component.find( + '[data-test-subj="filter-sets-applyToAllFilters"]' + ); + expect(applyToAllFiltersButton.first().prop('disabled')).toBe(false); + }); + + it('should render the language switcher panel', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + showFilterBar: true, + showQueryInput: true, + }; + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + const languageSwitcher = component.find('[data-test-subj="switchQueryLanguageButton"]'); + expect(languageSwitcher.length).toBeTruthy(); + }); + + it('should render the save query quick buttons', async () => { + const newProps = { + ...props, + openQueryBarMenu: true, + showSaveQuery: true, + filters: [ + { + meta: { + index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'category.keyword', + params: { + query: "Men's Accessories", + }, + }, + query: { + match_phrase: { + 'category.keyword': "Men's Accessories", + }, + }, + $state: { + store: 'appState', + }, + }, + ] as Filter[], + savedQuery: { + id: '8a0b7cd0-b0c4-11ec-92b2-73d62e0d28a9', + attributes: { + title: 'Test', + description: '', + query: { + query: 'category.keyword : "Men\'s Shoes" ', + language: 'kuery', + }, + filters: [], + }, + }, + }; + const component = mount(wrapQueryBarMenuComponentInContext(newProps, 'kuery')); + const saveChangesButton = component.find( + '[data-test-subj="saved-query-management-save-changes-button"]' + ); + expect(saveChangesButton.length).toBeTruthy(); + const saveChangesAsNewButton = component.find( + '[data-test-subj="saved-query-management-save-as-new-button"]' + ); + expect(saveChangesAsNewButton.length).toBeTruthy(); + }); +}); diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx new file mode 100644 index 0000000000000..2b34aef33eeee --- /dev/null +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useEffect } from 'react'; +import { + EuiButtonIcon, + EuiContextMenu, + EuiContextMenuPanel, + EuiPopover, + useGeneratedHtmlId, + EuiButtonIconProps, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { Filter, Query } from '@kbn/es-query'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { TimeRange, SavedQueryService, SavedQuery } from '@kbn/data-plugin/public'; +import { QueryBarMenuPanels } from './query_bar_menu_panels'; +import { FilterEditorWrapper } from './filter_editor_wrapper'; + +export interface QueryBarMenuProps { + language: string; + onQueryChange: (payload: { dateRange: TimeRange; query?: Query }) => void; + onQueryBarSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void; + toggleFilterBarMenuPopover: (value: boolean) => void; + openQueryBarMenu: boolean; + nonKqlMode?: 'lucene' | 'text'; + dateRangeFrom?: string; + dateRangeTo?: string; + savedQueryService: SavedQueryService; + saveAsNewQueryFormComponent?: JSX.Element; + saveFormComponent?: JSX.Element; + manageFilterSetComponent?: JSX.Element; + onFiltersUpdated?: (filters: Filter[]) => void; + filters?: Filter[]; + query?: Query; + savedQuery?: SavedQuery; + onClearSavedQuery?: () => void; + showQueryInput?: boolean; + showFilterBar?: boolean; + showSaveQuery?: boolean; + timeRangeForSuggestionsOverride?: boolean; + indexPatterns?: Array; + buttonProps?: Partial; +} + +export function QueryBarMenu({ + language, + nonKqlMode, + dateRangeFrom, + dateRangeTo, + onQueryChange, + onQueryBarSubmit, + savedQueryService, + saveAsNewQueryFormComponent, + saveFormComponent, + manageFilterSetComponent, + openQueryBarMenu, + toggleFilterBarMenuPopover, + onFiltersUpdated, + filters, + query, + savedQuery, + onClearSavedQuery, + showQueryInput, + showFilterBar, + showSaveQuery, + indexPatterns, + timeRangeForSuggestionsOverride, + buttonProps, +}: QueryBarMenuProps) { + const [renderedComponent, setRenderedComponent] = useState('menu'); + + useEffect(() => { + if (openQueryBarMenu) { + setRenderedComponent('menu'); + } + }, [openQueryBarMenu]); + + const normalContextMenuPopoverId = useGeneratedHtmlId({ + prefix: 'normalContextMenuPopover', + }); + const onButtonClick = () => { + toggleFilterBarMenuPopover(!openQueryBarMenu); + }; + + const closePopover = () => { + toggleFilterBarMenuPopover(false); + }; + + const buttonLabel = i18n.translate('unifiedSearch.filter.options.filterSetButtonLabel', { + defaultMessage: 'Filter set menu', + }); + + const button = ( + + + + ); + + const panels = QueryBarMenuPanels({ + filters, + savedQuery, + language, + dateRangeFrom, + dateRangeTo, + query, + showSaveQuery, + showFilterBar, + showQueryInput, + savedQueryService, + saveAsNewQueryFormComponent, + manageFilterSetComponent, + nonKqlMode, + closePopover, + onQueryBarSubmit, + onFiltersUpdated, + onClearSavedQuery, + onQueryChange, + setRenderedComponent, + }); + + const renderComponent = () => { + switch (renderedComponent) { + case 'menu': + default: + return ( + + ); + case 'saveForm': + return ( + {saveFormComponent}]} /> + ); + case 'saveAsNewForm': + return ( + {saveAsNewQueryFormComponent}]} + /> + ); + case 'addFilter': + return ( + , + ]} + /> + ); + } + }; + + return ( + <> + + {renderComponent()} + + + ); +} diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx new file mode 100644 index 0000000000000..de70e66fda5fc --- /dev/null +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx @@ -0,0 +1,494 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { isEqual } from 'lodash'; +import { + EuiContextMenuPanelDescriptor, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; +import { + Filter, + Query, + enableFilter, + disableFilter, + toggleFilterNegated, + pinFilter, + unpinFilter, +} from '@kbn/es-query'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { KIBANA_USER_QUERY_LANGUAGE_KEY, UI_SETTINGS } from '@kbn/data-plugin/common'; +import type { + IDataPluginServices, + TimeRange, + SavedQueryService, + SavedQuery, +} from '@kbn/data-plugin/public'; +import { fromUser } from './from_user'; +import { QueryLanguageSwitcher } from './language_switcher'; + +interface QueryBarMenuPanelProps { + filters?: Filter[]; + savedQuery?: SavedQuery; + language: string; + dateRangeFrom?: string; + dateRangeTo?: string; + query?: Query; + showSaveQuery?: boolean; + showQueryInput?: boolean; + showFilterBar?: boolean; + savedQueryService: SavedQueryService; + saveAsNewQueryFormComponent?: JSX.Element; + manageFilterSetComponent?: JSX.Element; + nonKqlMode?: 'lucene' | 'text'; + closePopover: () => void; + onQueryBarSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void; + onFiltersUpdated?: (filters: Filter[]) => void; + onClearSavedQuery?: () => void; + onQueryChange: (payload: { dateRange: TimeRange; query?: Query }) => void; + setRenderedComponent: (component: string) => void; +} + +export function QueryBarMenuPanels({ + filters, + savedQuery, + language, + dateRangeFrom, + dateRangeTo, + query, + showSaveQuery, + showFilterBar, + showQueryInput, + savedQueryService, + saveAsNewQueryFormComponent, + manageFilterSetComponent, + nonKqlMode, + closePopover, + onQueryBarSubmit, + onFiltersUpdated, + onClearSavedQuery, + onQueryChange, + setRenderedComponent, +}: QueryBarMenuPanelProps) { + const kibana = useKibana(); + const { appName, usageCollection, uiSettings, http, storage } = kibana.services; + const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); + const cancelPendingListingRequest = useRef<() => void>(() => {}); + + const [savedQueries, setSavedQueries] = useState([] as SavedQuery[]); + const [hasFiltersOrQuery, setHasFiltersOrQuery] = useState(false); + const [savedQueryHasChanged, setSavedQueryHasChanged] = useState(false); + + useEffect(() => { + const fetchSavedQueries = async () => { + cancelPendingListingRequest.current(); + let requestGotCancelled = false; + cancelPendingListingRequest.current = () => { + requestGotCancelled = true; + }; + + const { queries: savedQueryItems } = await savedQueryService.findSavedQueries(''); + + if (requestGotCancelled) return; + + setSavedQueries(savedQueryItems.reverse().slice(0, 5)); + }; + if (showQueryInput && showFilterBar) { + fetchSavedQueries(); + } + }, [savedQueryService, savedQuery, showQueryInput, showFilterBar]); + + useEffect(() => { + if (savedQuery) { + let filtersHaveChanged = filters?.length !== savedQuery.attributes?.filters?.length; + if (filters?.length === savedQuery.attributes?.filters?.length) { + filtersHaveChanged = Boolean( + filters?.some( + (filter, index) => + !isEqual(filter.query, savedQuery.attributes?.filters?.[index]?.query) + ) + ); + } + if (filtersHaveChanged || !isEqual(query, savedQuery?.attributes.query)) { + setSavedQueryHasChanged(true); + } else { + setSavedQueryHasChanged(false); + } + } + }, [filters, query, savedQuery, savedQuery?.attributes.filters, savedQuery?.attributes.query]); + + useEffect(() => { + const hasFilters = Boolean(filters && filters.length > 0); + const hasQuery = Boolean(query && query.query); + setHasFiltersOrQuery(hasFilters || hasQuery); + }, [filters, onClearSavedQuery, query, savedQuery]); + + const getDateRange = () => { + const defaultTimeSetting = uiSettings!.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS); + return { + from: dateRangeFrom || defaultTimeSetting.from, + to: dateRangeTo || defaultTimeSetting.to, + }; + }; + + const handleSaveAsNew = useCallback(() => { + setRenderedComponent('saveAsNewForm'); + }, [setRenderedComponent]); + + const handleSave = useCallback(() => { + setRenderedComponent('saveForm'); + }, [setRenderedComponent]); + + const onEnableAll = () => { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:enable_all`); + const enabledFilters = filters?.map(enableFilter); + if (enabledFilters) { + onFiltersUpdated?.(enabledFilters); + } + }; + + const onDisableAll = () => { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:disable_all`); + const disabledFilters = filters?.map(disableFilter); + if (disabledFilters) { + onFiltersUpdated?.(disabledFilters); + } + }; + + const onToggleAllNegated = () => { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:invert_all`); + const negatedFilters = filters?.map(toggleFilterNegated); + if (negatedFilters) { + onFiltersUpdated?.(negatedFilters); + } + }; + + const onRemoveAll = () => { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:remove_all`); + onFiltersUpdated?.([]); + }; + + const onPinAll = () => { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:pin_all`); + const pinnedFilters = filters?.map(pinFilter); + if (pinnedFilters) { + onFiltersUpdated?.(pinnedFilters); + } + }; + + const onUnpinAll = () => { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:unpin_all`); + const unPinnedFilters = filters?.map(unpinFilter); + if (unPinnedFilters) { + onFiltersUpdated?.(unPinnedFilters); + } + }; + + const onQueryStringChange = (value: string) => { + onQueryChange({ + query: { query: value, language }, + dateRange: getDateRange(), + }); + }; + + const onSelectLanguage = (lang: string) => { + http.post('/api/kibana/kql_opt_in_stats', { + body: JSON.stringify({ opt_in: lang === 'kuery' }), + }); + + const storageKey = KIBANA_USER_QUERY_LANGUAGE_KEY; + storage.set(storageKey!, lang); + + const newQuery = { query: '', language: lang }; + onQueryStringChange(newQuery.query); + onQueryBarSubmit({ + query: { query: fromUser(newQuery.query), language: newQuery.language }, + dateRange: getDateRange(), + }); + }; + + const luceneLabel = i18n.translate('unifiedSearch.query.queryBar.luceneLanguageName', { + defaultMessage: 'Lucene', + }); + const kqlLabel = i18n.translate('unifiedSearch.query.queryBar.kqlLanguageName', { + defaultMessage: 'KQL', + }); + + const filtersRelatedPanels = [ + { + name: i18n.translate('unifiedSearch.filter.options.addFilterButtonLabel', { + defaultMessage: 'Add filter', + }), + icon: 'plus', + onClick: () => { + setRenderedComponent('addFilter'); + }, + }, + { + name: i18n.translate('unifiedSearch.filter.options.applyAllFiltersButtonLabel', { + defaultMessage: 'Apply to all', + }), + icon: 'filter', + panel: 2, + disabled: !Boolean(filters && filters.length > 0), + 'data-test-subj': 'filter-sets-applyToAllFilters', + }, + ]; + + const queryAndFiltersRelatedPanels = [ + { + name: savedQuery + ? i18n.translate('unifiedSearch.filter.options.loadOtherFilterSetLabel', { + defaultMessage: 'Load other filter set', + }) + : i18n.translate('unifiedSearch.filter.options.loadCurrentFilterSetLabel', { + defaultMessage: 'Load filter set', + }), + panel: 4, + width: 350, + icon: 'filter', + 'data-test-subj': 'saved-query-management-load-button', + disabled: !savedQueries.length, + }, + { + name: savedQuery + ? i18n.translate('unifiedSearch.filter.options.saveAsNewFilterSetLabel', { + defaultMessage: 'Save as new', + }) + : i18n.translate('unifiedSearch.filter.options.saveFilterSetLabel', { + defaultMessage: 'Save filter set', + }), + icon: 'save', + disabled: + !Boolean(showSaveQuery) || !hasFiltersOrQuery || (savedQuery && !savedQueryHasChanged), + panel: 1, + 'data-test-subj': 'saved-query-management-save-button', + }, + { isSeparator: true }, + ]; + + const items = []; + // apply to all actions are only shown when there are filters + if (showFilterBar) { + items.push(...filtersRelatedPanels); + } + // clear all actions are only shown when there are filters or query + if (showFilterBar || showQueryInput) { + items.push( + { + name: i18n.translate('unifiedSearch.filter.options.clearllFiltersButtonLabel', { + defaultMessage: 'Clear all', + }), + disabled: !hasFiltersOrQuery && !Boolean(savedQuery), + icon: 'crossInACircleFilled', + 'data-test-subj': 'filter-sets-removeAllFilters', + onClick: () => { + closePopover(); + onQueryBarSubmit({ + query: { query: '', language }, + dateRange: getDateRange(), + }); + onRemoveAll(); + onClearSavedQuery?.(); + }, + }, + { isSeparator: true } + ); + } + // saved queries actions are only shown when the showQueryInput and showFilterBar is true + if (showQueryInput && showFilterBar) { + items.push(...queryAndFiltersRelatedPanels); + } + + // language menu appears when the showQueryInput is true + if (showQueryInput) { + items.push({ + name: `Language: ${language === 'kuery' ? kqlLabel : luceneLabel}`, + panel: 3, + 'data-test-subj': 'switchQueryLanguageButton', + }); + } + + const panels = [ + { + id: 0, + title: ( + <> + + + + {savedQuery ? savedQuery.attributes.title : 'Filter set'} + + + {savedQuery && savedQueryHasChanged && Boolean(showSaveQuery) && hasFiltersOrQuery && ( + + + + + {i18n.translate( + 'unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText', + { + defaultMessage: 'Save changes', + } + )} + + + + + {i18n.translate( + 'unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText', + { + defaultMessage: 'Save as new', + } + )} + + + + + )} + + + ), + items, + }, + { + id: 1, + title: i18n.translate('unifiedSearch.filter.options.saveCurrentFilterSetLabel', { + defaultMessage: 'Save current filter set', + }), + disabled: !Boolean(showSaveQuery), + content:
{saveAsNewQueryFormComponent}
, + }, + { + id: 2, + initialFocusedItemIndex: 1, + title: i18n.translate('unifiedSearch.filter.options.applyAllFiltersButtonLabel', { + defaultMessage: 'Apply to all', + }), + items: [ + { + name: i18n.translate('unifiedSearch.filter.options.enableAllFiltersButtonLabel', { + defaultMessage: 'Enable all', + }), + icon: 'eye', + 'data-test-subj': 'filter-sets-enableAllFilters', + onClick: () => { + closePopover(); + onEnableAll(); + }, + }, + { + name: i18n.translate('unifiedSearch.filter.options.disableAllFiltersButtonLabel', { + defaultMessage: 'Disable all', + }), + 'data-test-subj': 'filter-sets-disableAllFilters', + icon: 'eyeClosed', + onClick: () => { + closePopover(); + onDisableAll(); + }, + }, + { + name: i18n.translate('unifiedSearch.filter.options.invertNegatedFiltersButtonLabel', { + defaultMessage: 'Invert inclusion', + }), + 'data-test-subj': 'filter-sets-invertAllFilters', + icon: 'invert', + onClick: () => { + closePopover(); + onToggleAllNegated(); + }, + }, + { + name: i18n.translate('unifiedSearch.filter.options.pinAllFiltersButtonLabel', { + defaultMessage: 'Pin all', + }), + 'data-test-subj': 'filter-sets-pinAllFilters', + icon: 'pin', + onClick: () => { + closePopover(); + onPinAll(); + }, + }, + { + name: i18n.translate('unifiedSearch.filter.options.unpinAllFiltersButtonLabel', { + defaultMessage: 'Unpin all', + }), + 'data-test-subj': 'filter-sets-unpinAllFilters', + icon: 'pin', + onClick: () => { + closePopover(); + onUnpinAll(); + }, + }, + ], + }, + { + id: 3, + title: i18n.translate('unifiedSearch.filter.options.filterLanguageLabel', { + defaultMessage: 'Filter language', + }), + content: ( + + ), + }, + { + id: 4, + title: i18n.translate('unifiedSearch.filter.options.loadCurrentFilterSetLabel', { + defaultMessage: 'Load filter set', + }), + width: 400, + content:
{manageFilterSetComponent}
, + }, + ] as EuiContextMenuPanelDescriptor[]; + + return panels; +} diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index de1fa659aa133..bb01338d8d5a0 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -11,6 +11,7 @@ import classNames from 'classnames'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import deepEqual from 'fast-deep-equal'; import useObservable from 'react-use/lib/useObservable'; +import type { Filter } from '@kbn/es-query'; import { EMPTY } from 'rxjs'; import { map } from 'rxjs/operators'; import { @@ -20,8 +21,9 @@ import { EuiFieldText, usePrettyDuration, EuiIconProps, - EuiSuperUpdateButton, OnRefreshProps, + useIsWithinBreakpoints, + EuiSuperUpdateButton, } from '@elastic/eui'; import { IDataPluginServices, @@ -30,20 +32,21 @@ import { Query, getQueryLog, } from '@kbn/data-plugin/public'; +import { i18n } from '@kbn/i18n'; import { DataView } from '@kbn/data-views-plugin/public'; import type { PersistedLog } from '@kbn/data-plugin/public'; import { useKibana, withKibana } from '@kbn/kibana-react-plugin/public'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import QueryStringInputUI from './query_string_input'; import { NoDataPopover } from './no_data_popover'; -import { shallowEqual } from '../utils'; +import { shallowEqual } from '../utils/shallow_equal'; +import { AddFilterPopover } from './add_filter_popover'; +import { DataViewPicker, DataViewPickerProps } from '../dataview_picker'; +import { FilterButtonGroup } from '../filter_bar/filter_button_group/filter_button_group'; const SuperDatePicker = React.memo( EuiSuperDatePicker as any ) as unknown as typeof EuiSuperDatePicker; -const SuperUpdateButton = React.memo( - EuiSuperUpdateButton as any -) as unknown as typeof EuiSuperUpdateButton; const QueryStringInput = withKibana(QueryStringInputUI); @@ -63,7 +66,6 @@ export interface QueryBarTopRowProps { isLoading?: boolean; isRefreshPaused?: boolean; nonKqlMode?: 'lucene' | 'text'; - nonKqlModeHelpText?: string; onChange: (payload: { dateRange: TimeRange; query?: Query }) => void; onRefresh?: (payload: { dateRange: TimeRange }) => void; onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; @@ -74,10 +76,17 @@ export interface QueryBarTopRowProps { refreshInterval?: number; screenTitle?: string; showQueryInput?: boolean; + showAddFilter?: boolean; showDatePicker?: boolean; showAutoRefreshOnly?: boolean; timeHistory?: TimeHistoryContract; timeRangeForSuggestionsOverride?: boolean; + filters: Filter[]; + onFiltersUpdated?: (filters: Filter[]) => void; + dataViewPickerComponentProps?: DataViewPickerProps; + filterBar?: React.ReactNode; + showDatePickerAsBadge?: boolean; + showSubmitButton?: boolean; } const SharingMetaFields = React.memo(function SharingMetaFields({ @@ -114,7 +123,13 @@ const SharingMetaFields = React.memo(function SharingMetaFields({ export const QueryBarTopRow = React.memo( function QueryBarTopRow(props: QueryBarTopRowProps) { - const { showQueryInput = true, showDatePicker = true, showAutoRefreshOnly = false } = props; + const isMobile = useIsWithinBreakpoints(['xs', 's']); + const { + showQueryInput = true, + showDatePicker = true, + showAutoRefreshOnly = false, + showSubmitButton = true, + } = props; const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); const [isQueryInputFocused, setIsQueryInputFocused] = useState(false); @@ -283,14 +298,27 @@ export const QueryBarTopRow = React.memo( return Boolean(showDatePicker || showAutoRefreshOnly); } + function renderFilterMenuOnly(): boolean { + return !Boolean(props.showAddFilter) && Boolean(props.prepend); + } + + function shouldRenderUpdatebutton(): boolean { + return ( + Boolean(showSubmitButton) && + Boolean(showQueryInput || showDatePicker || showAutoRefreshOnly) + ); + } + + function shouldShowDatePickerAsBadge(): boolean { + return Boolean(props.showDatePickerAsBadge) && !shouldRenderQueryInput(); + } + function renderDatePicker() { if (!shouldRenderDatePicker()) { return null; } - const wrapperClasses = classNames('kbnQueryBar__datePickerWrapper', { - 'kbnQueryBar__datePickerWrapper-isHidden': isQueryInputFocused, - }); + const wrapperClasses = classNames('kbnQueryBar__datePickerWrapper'); return ( @@ -308,23 +336,52 @@ export const QueryBarTopRow = React.memo( dateFormat={uiSettings.get('dateFormat')} isAutoRefreshOnly={showAutoRefreshOnly} className="kbnQueryBar__datePicker" + isQuickSelectOnly={isMobile ? false : isQueryInputFocused} + width={isMobile ? 'full' : 'auto'} + compressed={shouldShowDatePickerAsBadge()} /> ); } function renderUpdateButton() { + if (!shouldRenderUpdatebutton()) { + return null; + } + + const buttonLabelUpdate = i18n.translate('unifiedSearch.queryBarTopRow.submitButton.update', { + defaultMessage: 'Needs updating', + }); + const buttonLabelRefresh = i18n.translate( + 'unifiedSearch.queryBarTopRow.submitButton.refresh', + { + defaultMessage: 'Refresh query', + } + ); + const button = props.customSubmitButton ? ( React.cloneElement(props.customSubmitButton, { onClick: onClickSubmitButton }) ) : ( - + + + ); if (!shouldRenderDatePicker()) { @@ -332,61 +389,118 @@ export const QueryBarTopRow = React.memo( } return ( - - - {renderDatePicker()} - {button} - - + + + + {renderDatePicker()} + {button} + + + ); } - function renderQueryInput() { - if (!shouldRenderQueryInput()) return; + function renderDataViewsPicker() { + if (!props.dataViewPickerComponentProps) return; return ( - - + ); } - const classes = classNames('kbnQueryBar', { - 'kbnQueryBar--withDatePicker': showDatePicker, - }); + function renderAddButton() { + return ( + Boolean(props.showAddFilter) && ( + + + + ) + ); + } + + function renderFilterButtonGroup() { + return ( + (Boolean(props.showAddFilter) || Boolean(props.prepend)) && ( + + + + ) + ); + } + + function renderQueryInput() { + return ( + + {!renderFilterMenuOnly() && renderFilterButtonGroup()} + {shouldRenderQueryInput() && ( + + + + )} + + ); + } return ( - - {renderQueryInput()} + <> - {renderUpdateButton()} - + + {renderDataViewsPicker()} + + {renderQueryInput()} + + {shouldShowDatePickerAsBadge() && props.filterBar} + {renderUpdateButton()} + + {!shouldShowDatePickerAsBadge() && props.filterBar} + ); }, ({ query: prevQuery, ...prevProps }, { query: nextQuery, ...nextProps }) => { diff --git a/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx b/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx index b4eed13da7f58..7437bf5fd4ece 100644 --- a/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx @@ -110,7 +110,6 @@ describe('QueryStringInput', () => { ); await waitFor(() => getByText(kqlQuery.query)); - await waitFor(() => getByText('KQL')); }); it('Should pass the query language to the language switcher', () => { diff --git a/src/plugins/unified_search/public/query_string_input/query_string_input.tsx b/src/plugins/unified_search/public/query_string_input/query_string_input.tsx index a9f4127809ab7..31ff302f9e699 100644 --- a/src/plugins/unified_search/public/query_string_input/query_string_input.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_string_input.tsx @@ -24,9 +24,10 @@ import { EuiTextArea, htmlIdGenerator, PopoverAnchorPosition, + toSentenceCase, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { compact, debounce, isEqual, isFunction } from 'lodash'; +import { compact, debounce, isEmpty, isEqual, isFunction } from 'lodash'; import { Toast } from '@kbn/core/public'; import { IDataPluginServices, Query, getQueryLog } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; @@ -41,6 +42,7 @@ import { QueryLanguageSwitcher } from './language_switcher'; import type { SuggestionsListSize } from '../typeahead/suggestions_component'; import { SuggestionsComponent } from '../typeahead'; import { onRaf } from '../utils'; +import { FilterButtonGroup } from '../filter_bar/filter_button_group/filter_button_group'; import { QuerySuggestion, QuerySuggestionTypes } from '../autocomplete'; import { getTheme, getAutocomplete } from '../services'; @@ -72,7 +74,6 @@ export interface QueryStringInputProps { * this params add another option text, which is just a simple keyword search mode, the way a simple search box works */ nonKqlMode?: 'lucene' | 'text'; - nonKqlModeHelpText?: string; /** * @param autoSubmit if user selects a value, in that case kuery will be auto submitted */ @@ -124,6 +125,8 @@ const KEY_CODES = { export default class QueryStringInputUI extends PureComponent { static defaultProps = { storageKey: KIBANA_USER_QUERY_LANGUAGE_KEY, + iconType: 'search', + isClearable: true, }; public state: State = { @@ -678,31 +681,59 @@ export default class QueryStringInputUI extends PureComponent { this.handleAutoHeight(); }; + getSearchInputPlaceholder = () => { + let placeholder = ''; + if (!this.props.query.language || this.props.query.language === 'text') { + placeholder = i18n.translate('unifiedSearch.query.queryBar.searchInputPlaceholderForText', { + defaultMessage: 'Filter your data', + }); + } else { + const language = + this.props.query.language === 'kuery' ? 'KQL' : toSentenceCase(this.props.query.language); + + placeholder = i18n.translate('unifiedSearch.query.queryBar.searchInputPlaceholder', { + defaultMessage: 'Filter your data using {language} syntax', + values: { language }, + }); + } + + return placeholder; + }; + public render() { const isSuggestionsVisible = this.state.isSuggestionsVisible && { 'aria-controls': 'kbnTypeahead__items', 'aria-owns': 'kbnTypeahead__items', }; const ariaCombobox = { ...isSuggestionsVisible, role: 'combobox' }; - const containerClassName = classNames( - 'euiFormControlLayout euiFormControlLayout--group kbnQueryBar__wrap', - this.props.className - ); - const inputClassName = classNames( - 'kbnQueryBar__textarea', - this.props.iconType ? 'kbnQueryBar__textarea--withIcon' : null, - this.props.prepend ? 'kbnQueryBar__textarea--hasPrepend' : null, - !this.props.disableLanguageSwitcher ? 'kbnQueryBar__textarea--hasAppend' : null - ); - const inputWrapClassName = classNames( - 'euiFormControlLayout__childrenWrapper kbnQueryBar__textareaWrap', - this.props.prepend ? 'kbnQueryBar__textareaWrap--hasPrepend' : null, - !this.props.disableLanguageSwitcher ? 'kbnQueryBar__textareaWrap--hasAppend' : null + + const simpleLanguageSwitcher = this.props.disableLanguageSwitcher ? null : ( + ); + const prependElement = + this.props.prepend || simpleLanguageSwitcher ? ( + + ) : undefined; + + const containerClassName = classNames('kbnQueryBar__wrap', this.props.className); + const inputClassName = classNames('kbnQueryBar__textarea', { + 'kbnQueryBar__textarea--withIcon': this.props.iconType, + 'kbnQueryBar__textarea--isClearable': this.props.isClearable, + 'kbnQueryBar__textarea--withPrepend': prependElement, + 'kbnQueryBar__textarea--isSuggestionsVisible': + isSuggestionsVisible && !isEmpty(this.state.suggestions), + }); + const inputWrapClassName = classNames('kbnQueryBar__textareaWrap'); return (
- {this.props.prepend} + {prependElement} +
{ >
{
- {this.props.disableLanguageSwitcher ? null : ( - - )}
); } diff --git a/src/plugins/unified_search/public/saved_query_form/save_query_form.tsx b/src/plugins/unified_search/public/saved_query_form/save_query_form.tsx index 53c5ec3310da2..186c1f072aedd 100644 --- a/src/plugins/unified_search/public/saved_query_form/save_query_form.tsx +++ b/src/plugins/unified_search/public/saved_query_form/save_query_form.tsx @@ -7,20 +7,7 @@ */ import React, { useEffect, useState, useCallback } from 'react'; -import { - EuiButtonEmpty, - EuiModal, - EuiButton, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, - EuiModalFooter, - EuiForm, - EuiFormRow, - EuiFieldText, - EuiSwitch, - EuiText, -} from '@elastic/eui'; +import { EuiButton, EuiForm, EuiFormRow, EuiFieldText, EuiSwitch } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { sortBy, isEqual } from 'lodash'; import { SavedQuery, SavedQueryService } from '@kbn/data-plugin/public'; @@ -51,8 +38,6 @@ export function SaveQueryForm({ showTimeFilterOption = true, }: Props) { const [title, setTitle] = useState(savedQuery?.attributes.title ?? ''); - const [enabledSaveButton, setEnabledSaveButton] = useState(Boolean(savedQuery)); - const [description, setDescription] = useState(savedQuery?.attributes.description ?? ''); const [savedQueries, setSavedQueries] = useState([]); const [shouldIncludeFilters, setShouldIncludeFilters] = useState( Boolean(savedQuery?.attributes.filters ?? true) @@ -72,10 +57,10 @@ export function SaveQueryForm({ } ); - const savedQueryDescriptionText = i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryDescriptionText', + const titleExistsErrorText = i18n.translate( + 'unifiedSearch.search.searchBar.savedQueryForm.titleExistsText', { - defaultMessage: 'Save query text and filters that you want to use again.', + defaultMessage: 'Name is required.', } ); @@ -98,36 +83,40 @@ export function SaveQueryForm({ errors.push(titleConflictErrorText); } + if (!title) { + errors.push(titleExistsErrorText); + } + if (!isEqual(errors, formErrors)) { setFormErrors(errors); return false; } return !formErrors.length; - }, [savedQueries, savedQuery, title, titleConflictErrorText, formErrors]); + }, [savedQueries, formErrors, title, savedQuery, titleConflictErrorText, titleExistsErrorText]); const onClickSave = useCallback(() => { if (validate()) { onSave({ id: savedQuery?.id, title, - description, + description: '', shouldIncludeFilters, shouldIncludeTimefilter, }); + onClose(); } }, [ validate, onSave, + onClose, savedQuery?.id, title, - description, shouldIncludeFilters, shouldIncludeTimefilter, ]); const onInputChange = useCallback((event) => { - setEnabledSaveButton(Boolean(event.target.value)); setFormErrors([]); setTitle(event.target.value); }, []); @@ -143,18 +132,16 @@ export function SaveQueryForm({ const saveQueryForm = ( - - {savedQueryDescriptionText} - - - { - setDescription(event.target.value); - }} - data-test-subj="saveQueryFormDescription" - /> - {showFilterOption && ( - + + )} - - ); - - return ( - - - - {i18n.translate('unifiedSearch.search.searchBar.savedQueryFormTitle', { - defaultMessage: 'Save query', - })} - - - - {saveQueryForm} - - - - {i18n.translate('unifiedSearch.search.searchBar.savedQueryFormCancelButtonText', { - defaultMessage: 'Cancel', - })} - + {i18n.translate('unifiedSearch.search.searchBar.savedQueryFormSaveButtonText', { - defaultMessage: 'Save', + defaultMessage: 'Save filter set', })} - - + + ); + + return <>{saveQueryForm}; } diff --git a/src/plugins/unified_search/public/saved_query_management/_index.scss b/src/plugins/unified_search/public/saved_query_management/_index.scss index 0580e857e8494..0c90d7817b685 100644 --- a/src/plugins/unified_search/public/saved_query_management/_index.scss +++ b/src/plugins/unified_search/public/saved_query_management/_index.scss @@ -1,2 +1 @@ -@import './saved_query_management_component'; -@import './saved_query_list_item'; +@import './saved_query_management_list'; diff --git a/src/plugins/unified_search/public/saved_query_management/_saved_query_list_item.scss b/src/plugins/unified_search/public/saved_query_management/_saved_query_list_item.scss deleted file mode 100644 index 714ba82dfb476..0000000000000 --- a/src/plugins/unified_search/public/saved_query_management/_saved_query_list_item.scss +++ /dev/null @@ -1,21 +0,0 @@ -.kbnSavedQueryListItem { - margin-top: 0; - color: $euiLinkColor; -} - -// Can't actually target the button with classes, but styles to override -// are just user agent styles -.kbnSavedQueryListItem-selected button { - font-weight: $euiFontWeightBold; -} - -// This will ensure the info icon and tooltip shows even if the label gets truncated -.kbnSavedQueryListItem__label { - display: flex; - align-items: center; -} - -.kbnSavedQueryListItem__labelText { - @include euiTextTruncate; - margin-right: $euiSizeXS; -} diff --git a/src/plugins/unified_search/public/saved_query_management/_saved_query_management_component.scss b/src/plugins/unified_search/public/saved_query_management/_saved_query_management_list.scss similarity index 76% rename from src/plugins/unified_search/public/saved_query_management/_saved_query_management_component.scss rename to src/plugins/unified_search/public/saved_query_management/_saved_query_management_list.scss index 928cb5a34d6de..7ce304310ae56 100644 --- a/src/plugins/unified_search/public/saved_query_management/_saved_query_management_component.scss +++ b/src/plugins/unified_search/public/saved_query_management/_saved_query_management_list.scss @@ -1,18 +1,9 @@ -.kbnSavedQueryManagement__popover { - max-width: $euiFormMaxWidth; -} - .kbnSavedQueryManagement__listWrapper { // Addition height will ensure one item is "cutoff" to indicate more below the scroll max-height: $euiFormMaxWidth + $euiSize; overflow-y: hidden; } -.kbnSavedQueryManagement__pagination { - justify-content: center; - padding: ($euiSizeM / 2) $euiSizeM $euiSizeM; -} - .kbnSavedQueryManagement__text { padding: $euiSizeM $euiSizeM ($euiSizeM / 2) $euiSizeM; } diff --git a/src/plugins/unified_search/public/saved_query_management/index.ts b/src/plugins/unified_search/public/saved_query_management/index.ts index 4ead1907cd23b..134b24a4fb85c 100644 --- a/src/plugins/unified_search/public/saved_query_management/index.ts +++ b/src/plugins/unified_search/public/saved_query_management/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { SavedQueryManagementComponent } from './saved_query_management_component'; +export { SavedQueryManagementList } from './saved_query_management_list'; diff --git a/src/plugins/unified_search/public/saved_query_management/saved_query_list_item.tsx b/src/plugins/unified_search/public/saved_query_management/saved_query_list_item.tsx deleted file mode 100644 index 71fbd8aad6e48..0000000000000 --- a/src/plugins/unified_search/public/saved_query_management/saved_query_list_item.tsx +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EuiListGroupItem, EuiConfirmModal, EuiIconTip } from '@elastic/eui'; - -import React, { Fragment, useState } from 'react'; -import classNames from 'classnames'; -import { i18n } from '@kbn/i18n'; -import { SavedQuery } from '@kbn/data-plugin/public'; - -interface Props { - savedQuery: SavedQuery; - isSelected: boolean; - showWriteOperations: boolean; - onSelect: (savedQuery: SavedQuery) => void; - onDelete: (savedQuery: SavedQuery) => void; -} - -export const SavedQueryListItem = ({ - savedQuery, - isSelected, - onSelect, - onDelete, - showWriteOperations, -}: Props) => { - const [showDeletionConfirmationModal, setShowDeletionConfirmationModal] = useState(false); - - const selectButtonAriaLabelText = isSelected - ? i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel', - { - defaultMessage: - 'Saved query button selected {savedQueryName}. Press to clear any changes.', - values: { savedQueryName: savedQuery.attributes.title }, - } - ) - : i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel', - { - defaultMessage: 'Saved query button {savedQueryName}', - values: { savedQueryName: savedQuery.attributes.title }, - } - ); - - const selectButtonDataTestSubj = isSelected - ? `load-saved-query-${savedQuery.attributes.title}-button saved-query-list-item-selected` - : `load-saved-query-${savedQuery.attributes.title}-button`; - - const classes = classNames('kbnSavedQueryListItem', { - 'kbnSavedQueryListItem-selected': isSelected, - }); - - const label = ( - - {savedQuery.attributes.title}{' '} - {savedQuery.attributes.description && ( - - )} - - ); - - return ( - - { - onSelect(savedQuery); - }} - aria-label={selectButtonAriaLabelText} - label={label} - iconType={isSelected ? 'check' : undefined} - extraAction={ - showWriteOperations - ? { - color: 'danger', - onClick: () => setShowDeletionConfirmationModal(true), - iconType: 'trash', - iconSize: 's', - 'aria-label': i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel', - { - defaultMessage: 'Delete saved query {savedQueryName}', - values: { savedQueryName: savedQuery.attributes.title }, - } - ), - title: i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel', - { - defaultMessage: 'Delete saved query {savedQueryName}', - values: { savedQueryName: savedQuery.attributes.title }, - } - ), - 'data-test-subj': `delete-saved-query-${savedQuery.attributes.title}-button`, - } - : undefined - } - /> - - {showDeletionConfirmationModal && ( - { - onDelete(savedQuery); - setShowDeletionConfirmationModal(false); - }} - buttonColor="danger" - onCancel={() => { - setShowDeletionConfirmationModal(false); - }} - /> - )} - - ); -}; diff --git a/src/plugins/unified_search/public/saved_query_management/saved_query_management_component.tsx b/src/plugins/unified_search/public/saved_query_management/saved_query_management_component.tsx deleted file mode 100644 index 07d3a9d799a66..0000000000000 --- a/src/plugins/unified_search/public/saved_query_management/saved_query_management_component.tsx +++ /dev/null @@ -1,340 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { - EuiPopover, - EuiPopoverTitle, - EuiPopoverFooter, - EuiButtonEmpty, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiListGroup, - EuiPagination, - EuiText, - EuiSpacer, - EuiIcon, -} from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import React, { useCallback, useEffect, useState, Fragment, useRef } from 'react'; -import { sortBy } from 'lodash'; -import { SavedQuery, SavedQueryService } from '@kbn/data-plugin/public'; -import { SavedQueryListItem } from './saved_query_list_item'; - -const perPage = 50; -interface Props { - showSaveQuery?: boolean; - loadedSavedQuery?: SavedQuery; - savedQueryService: SavedQueryService; - onSave: () => void; - onSaveAsNew: () => void; - onLoad: (savedQuery: SavedQuery) => void; - onClearSavedQuery: () => void; -} - -export function SavedQueryManagementComponent({ - showSaveQuery, - loadedSavedQuery, - onSave, - onSaveAsNew, - onLoad, - onClearSavedQuery, - savedQueryService, -}: Props) { - const [isOpen, setIsOpen] = useState(false); - const [savedQueries, setSavedQueries] = useState([] as SavedQuery[]); - const [count, setTotalCount] = useState(0); - const [activePage, setActivePage] = useState(0); - const cancelPendingListingRequest = useRef<() => void>(() => {}); - - useEffect(() => { - const fetchCountAndSavedQueries = async () => { - cancelPendingListingRequest.current(); - let requestGotCancelled = false; - cancelPendingListingRequest.current = () => { - requestGotCancelled = true; - }; - - const { total: savedQueryCount, queries: savedQueryItems } = - await savedQueryService.findSavedQueries('', perPage, activePage + 1); - - if (requestGotCancelled) return; - - const sortedSavedQueryItems = sortBy(savedQueryItems, 'attributes.title'); - setTotalCount(savedQueryCount); - setSavedQueries(sortedSavedQueryItems); - }; - if (isOpen) { - fetchCountAndSavedQueries(); - } - }, [isOpen, activePage, savedQueryService]); - - const handleTogglePopover = useCallback( - () => setIsOpen((currentState) => !currentState), - [setIsOpen] - ); - - const handleClosePopover = useCallback(() => setIsOpen(false), []); - - const handleSave = useCallback(() => { - handleClosePopover(); - onSave(); - }, [handleClosePopover, onSave]); - - const handleSaveAsNew = useCallback(() => { - handleClosePopover(); - onSaveAsNew(); - }, [handleClosePopover, onSaveAsNew]); - - const handleSelect = useCallback( - (savedQueryToSelect) => { - handleClosePopover(); - onLoad(savedQueryToSelect); - }, - [handleClosePopover, onLoad] - ); - - const handleDelete = useCallback( - (savedQueryToDelete: SavedQuery) => { - const onDeleteSavedQuery = async (savedQuery: SavedQuery) => { - cancelPendingListingRequest.current(); - setSavedQueries( - savedQueries.filter((currentSavedQuery) => currentSavedQuery.id !== savedQuery.id) - ); - - if (loadedSavedQuery && loadedSavedQuery.id === savedQuery.id) { - onClearSavedQuery(); - } - - await savedQueryService.deleteSavedQuery(savedQuery.id); - setActivePage(0); - }; - - onDeleteSavedQuery(savedQueryToDelete); - handleClosePopover(); - }, - [handleClosePopover, loadedSavedQuery, onClearSavedQuery, savedQueries, savedQueryService] - ); - - const savedQueryDescriptionText = i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryDescriptionText', - { - defaultMessage: 'Save query text and filters that you want to use again.', - } - ); - - const noSavedQueriesDescriptionText = - i18n.translate('unifiedSearch.search.searchBar.savedQueryNoSavedQueriesText', { - defaultMessage: 'There are no saved queries.', - }) + - ' ' + - savedQueryDescriptionText; - - const savedQueryPopoverTitleText = i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverTitleText', - { - defaultMessage: 'Saved Queries', - } - ); - - const goToPage = (pageNumber: number) => { - setActivePage(pageNumber); - }; - - const savedQueryPopoverButton = ( - - - - - ); - - const savedQueryRows = () => { - const savedQueriesWithoutCurrent = savedQueries.filter((savedQuery) => { - if (!loadedSavedQuery) return true; - return savedQuery.id !== loadedSavedQuery.id; - }); - const savedQueriesReordered = - loadedSavedQuery && savedQueriesWithoutCurrent.length !== savedQueries.length - ? [loadedSavedQuery, ...savedQueriesWithoutCurrent] - : [...savedQueriesWithoutCurrent]; - return savedQueriesReordered.map((savedQuery) => ( - - )); - }; - - return ( - - -
- - {savedQueryPopoverTitleText} - - {savedQueries.length > 0 ? ( - - -

{savedQueryDescriptionText}

-
-
- - {savedQueryRows()} - -
- -
- ) : ( - - -

{noSavedQueriesDescriptionText}

-
- -
- )} - - - {showSaveQuery && loadedSavedQuery && ( - - - - {i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText', - { - defaultMessage: 'Save changes', - } - )} - - - - - {i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText', - { - defaultMessage: 'Save as new', - } - )} - - - - )} - {showSaveQuery && !loadedSavedQuery && ( - - - {i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonText', - { - defaultMessage: 'Save current query', - } - )} - - - )} - - - {loadedSavedQuery && ( - - {i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverClearButtonText', - { - defaultMessage: 'Clear', - } - )} - - )} - - - -
-
-
- ); -} diff --git a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx new file mode 100644 index 0000000000000..7c2d0ebd1faad --- /dev/null +++ b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiSelectable } from '@elastic/eui'; +import { I18nProvider } from '@kbn/i18n-react'; +import { act } from 'react-dom/test-utils'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; +import { ReactWrapper } from 'enzyme'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { coreMock, applicationServiceMock } from '@kbn/core/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { + SavedQueryManagementListProps, + SavedQueryManagementList, +} from './saved_query_management_list'; + +describe('Saved query management list component', () => { + const startMock = coreMock.createStart(); + const dataMock = dataPluginMock.createStartContract(); + const applicationMock = applicationServiceMock.createStartContract(); + const application = { + ...applicationMock, + capabilities: { + ...applicationMock.capabilities, + savedObjectsManagement: { edit: true }, + }, + }; + function wrapSavedQueriesListComponentInContext(testProps: SavedQueryManagementListProps) { + const services = { + uiSettings: startMock.uiSettings, + http: startMock.http, + application, + }; + + return ( + + + + + + ); + } + + function flushEffect(component: ReactWrapper) { + return act(async () => { + await component; + await new Promise((r) => setImmediate(r)); + component.update(); + }); + } + let props: SavedQueryManagementListProps; + beforeEach(() => { + props = { + onLoad: jest.fn(), + onClearSavedQuery: jest.fn(), + onClose: jest.fn(), + showSaveQuery: true, + hasFiltersOrQuery: false, + savedQueryService: { + ...dataMock.query.savedQueries, + findSavedQueries: jest.fn().mockResolvedValue({ + queries: [ + { + id: '8a0b7cd0-b0c4-11ec-92b2-73d62e0d28a9', + attributes: { + title: 'Test', + description: '', + query: { + query: 'category.keyword : "Men\'s Shoes" ', + language: 'kuery', + }, + filters: [], + }, + }, + ], + }), + deleteSavedQuery: jest.fn(), + }, + }; + }); + it('should render the list component if saved queries exist', async () => { + const component = mount(wrapSavedQueriesListComponentInContext(props)); + await flushEffect(component); + expect(component.find('[data-test-subj="saved-query-management-list"]').length).toBe(1); + }); + + it('should not rendet the list component if not saved queries exist', async () => { + const newProps = { + ...props, + savedQueryService: { + ...dataMock.query.savedQueries, + findSavedQueries: jest.fn().mockResolvedValue({ + queries: [], + }), + }, + }; + const component = mount(wrapSavedQueriesListComponentInContext(newProps)); + await flushEffect(component); + expect(component.find('[data-test-subj="saved-query-management-empty"]').length).toBeTruthy(); + }); + + it('should render the saved queries on the selectable component', async () => { + const component = mount(wrapSavedQueriesListComponentInContext(props)); + await flushEffect(component); + expect(component.find(EuiSelectable).prop('options').length).toBe(1); + expect(component.find(EuiSelectable).prop('options')[0].label).toBe('Test'); + }); + + it('should call the onLoad function', async () => { + const onLoadSpy = jest.fn(); + const newProps = { + ...props, + onLoad: onLoadSpy, + }; + const component = mount(wrapSavedQueriesListComponentInContext(newProps)); + await flushEffect(component); + component.find('[data-test-subj="load-saved-query-Test-button"]').first().simulate('click'); + expect( + component.find('[data-test-subj="saved-query-management-apply-changes-button"]').length + ).toBeTruthy(); + component + .find('[data-test-subj="saved-query-management-apply-changes-button"]') + .first() + .simulate('click'); + expect(onLoadSpy).toBeCalled(); + }); + + it('should render the button with the correct text', async () => { + const component = mount(wrapSavedQueriesListComponentInContext(props)); + await flushEffect(component); + expect( + component + .find('[data-test-subj="saved-query-management-apply-changes-button"]') + .first() + .text() + ).toBe('Apply filter set'); + + const newProps = { + ...props, + hasFiltersOrQuery: true, + }; + const updatedComponent = mount(wrapSavedQueriesListComponentInContext(newProps)); + await flushEffect(component); + expect( + updatedComponent + .find('[data-test-subj="saved-query-management-apply-changes-button"]') + .first() + .text() + ).toBe('Replace with selected filter set'); + }); + + it('should render the modal on delete', async () => { + const component = mount(wrapSavedQueriesListComponentInContext(props)); + await flushEffect(component); + findTestSubject(component, 'delete-saved-query-Test-button').simulate('click'); + expect(component.find('[data-test-subj="confirmModalConfirmButton"]').length).toBeTruthy(); + }); +}); diff --git a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx new file mode 100644 index 0000000000000..7568bb9375fa6 --- /dev/null +++ b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx @@ -0,0 +1,385 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiSelectable, + EuiText, + EuiPopoverFooter, + EuiButtonIcon, + EuiButtonEmpty, + EuiConfirmModal, + usePrettyDuration, + ShortDate, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useEffect, useState, useRef } from 'react'; +import { css } from '@emotion/react'; +import { sortBy } from 'lodash'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { IDataPluginServices, SavedQuery, SavedQueryService } from '@kbn/data-plugin/public'; +import type { SavedQueryAttributes } from '@kbn/data-plugin/common'; + +export interface SavedQueryManagementListProps { + showSaveQuery?: boolean; + loadedSavedQuery?: SavedQuery; + savedQueryService: SavedQueryService; + onLoad: (savedQuery: SavedQuery) => void; + onClearSavedQuery: () => void; + onClose: () => void; + hasFiltersOrQuery: boolean; +} + +interface SelectableProps { + key?: string; + label: string; + value?: string; + checked?: 'on' | 'off' | undefined; +} + +interface DurationRange { + end: ShortDate; + label?: string; + start: ShortDate; +} + +const commonDurationRanges: DurationRange[] = [ + { start: 'now/d', end: 'now/d', label: 'Today' }, + { start: 'now/w', end: 'now/w', label: 'This week' }, + { start: 'now/M', end: 'now/M', label: 'This month' }, + { start: 'now/y', end: 'now/y', label: 'This year' }, + { start: 'now-1d/d', end: 'now-1d/d', label: 'Yesterday' }, + { start: 'now/w', end: 'now', label: 'Week to date' }, + { start: 'now/M', end: 'now', label: 'Month to date' }, + { start: 'now/y', end: 'now', label: 'Year to date' }, +]; + +const itemTitle = (attributes: SavedQueryAttributes, format: string) => { + let label = attributes.title; + const prettifier = usePrettyDuration; + + if (attributes.description) { + label += `; ${attributes.description}`; + } + + if (attributes.timefilter) { + label += `; ${prettifier({ + timeFrom: attributes.timefilter?.from, + timeTo: attributes.timefilter?.to, + quickRanges: commonDurationRanges, + dateFormat: format, + })}`; + } + + return label; +}; + +const itemLabel = (attributes: SavedQueryAttributes) => { + let label: React.ReactNode = attributes.title; + + if (attributes.description) { + label = ( + <> + {label} + + ); + } + + if (attributes.timefilter) { + label = ( + <> + {label} + + ); + } + + return label; +}; + +export function SavedQueryManagementList({ + showSaveQuery, + loadedSavedQuery, + onLoad, + onClearSavedQuery, + savedQueryService, + onClose, + hasFiltersOrQuery, +}: SavedQueryManagementListProps) { + const kibana = useKibana(); + const [savedQueries, setSavedQueries] = useState([] as SavedQuery[]); + const [selectedSavedQuery, setSelectedSavedQuery] = useState(null as SavedQuery | null); + const [toBeDeletedSavedQuery, setToBeDeletedSavedQuery] = useState(null as SavedQuery | null); + const [showDeletionConfirmationModal, setShowDeletionConfirmationModal] = useState(false); + const cancelPendingListingRequest = useRef<() => void>(() => {}); + const { uiSettings, http, application } = kibana.services; + const format = uiSettings.get('dateFormat'); + + useEffect(() => { + const fetchCountAndSavedQueries = async () => { + cancelPendingListingRequest.current(); + let requestGotCancelled = false; + cancelPendingListingRequest.current = () => { + requestGotCancelled = true; + }; + + const { queries: savedQueryItems } = await savedQueryService.findSavedQueries(); + + if (requestGotCancelled) return; + + const sortedSavedQueryItems = sortBy(savedQueryItems, 'attributes.title'); + setSavedQueries(sortedSavedQueryItems); + }; + fetchCountAndSavedQueries(); + }, [savedQueryService]); + + const handleLoad = useCallback(() => { + if (selectedSavedQuery) { + onLoad(selectedSavedQuery); + onClose(); + } + }, [onLoad, selectedSavedQuery, onClose]); + + const handleSelect = useCallback((savedQueryToSelect) => { + setSelectedSavedQuery(savedQueryToSelect); + }, []); + + const handleDelete = useCallback((savedQueryToDelete: SavedQuery) => { + setShowDeletionConfirmationModal(true); + setToBeDeletedSavedQuery(savedQueryToDelete); + }, []); + + const onDelete = useCallback( + (savedQueryToDelete: string) => { + const onDeleteSavedQuery = async (savedQueryId: string) => { + cancelPendingListingRequest.current(); + setSavedQueries( + savedQueries.filter((currentSavedQuery) => currentSavedQuery.id !== savedQueryId) + ); + + if (loadedSavedQuery && loadedSavedQuery.id === savedQueryId) { + onClearSavedQuery(); + } + + await savedQueryService.deleteSavedQuery(savedQueryId); + }; + + onDeleteSavedQuery(savedQueryToDelete); + }, + [loadedSavedQuery, onClearSavedQuery, savedQueries, savedQueryService] + ); + + const savedQueryDescriptionText = i18n.translate( + 'unifiedSearch.search.searchBar.savedQueryDescriptionText', + { + defaultMessage: 'Save query text and filters that you want to use again.', + } + ); + + const noSavedQueriesDescriptionText = + i18n.translate('unifiedSearch.search.searchBar.savedQueryNoSavedQueriesText', { + defaultMessage: 'No saved queries.', + }) + + ' ' + + savedQueryDescriptionText; + + const savedQueriesOptions = () => { + const savedQueriesWithoutCurrent = savedQueries.filter((savedQuery) => { + if (!loadedSavedQuery) return true; + return savedQuery.id !== loadedSavedQuery.id; + }); + const savedQueriesReordered = + loadedSavedQuery && savedQueriesWithoutCurrent.length !== savedQueries.length + ? [loadedSavedQuery, ...savedQueriesWithoutCurrent] + : [...savedQueriesWithoutCurrent]; + + return savedQueriesReordered.map((savedQuery) => { + return { + key: savedQuery.id, + label: itemLabel(savedQuery.attributes), + title: itemTitle(savedQuery.attributes, format), + 'data-test-subj': `load-saved-query-${savedQuery.attributes.title}-button`, + value: savedQuery.id, + checked: + (loadedSavedQuery && savedQuery.id === loadedSavedQuery.id) || + (selectedSavedQuery && savedQuery.id === selectedSavedQuery.id) + ? 'on' + : undefined, + append: !!showSaveQuery && ( + handleDelete(savedQuery)} + color="danger" + /> + ), + }; + }) as unknown as SelectableProps[]; + }; + + const canEditSavedObjects = application.capabilities.savedObjectsManagement.edit; + + const listComponent = ( + <> + {savedQueries.length > 0 ? ( + <> +
+ + aria-label="Basic example" + options={savedQueriesOptions()} + searchable + singleSelection="always" + onChange={(choices) => { + const choice = choices.find(({ checked }) => checked) as unknown as { + value: string; + }; + if (choice) { + handleSelect(savedQueries.find((savedQuery) => savedQuery.id === choice.value)); + } + }} + searchProps={{ + compressed: true, + placeholder: i18n.translate( + 'unifiedSearch.query.queryBar.indexPattern.findFilterSet', + { + defaultMessage: 'Find a filter set', + } + ), + }} + listProps={{ + isVirtualized: true, + }} + > + {(list, search) => ( + <> + + {search} + + {list} + + )} + +
+ + ) : ( + <> + +

{noSavedQueriesDescriptionText}

+
+ + )} + + + {canEditSavedObjects && ( + + + Manage + + + )} + + + {hasFiltersOrQuery + ? i18n.translate( + 'unifiedSearch.search.searchBar.savedQueryPopoverReplaceFilterSetLabel', + { + defaultMessage: 'Replace with selected filter set', + } + ) + : i18n.translate( + 'unifiedSearch.search.searchBar.savedQueryPopoverApplyFilterSetLabel', + { + defaultMessage: 'Apply filter set', + } + )} + + + + + {showDeletionConfirmationModal && toBeDeletedSavedQuery && ( + { + onDelete(toBeDeletedSavedQuery.id); + setShowDeletionConfirmationModal(false); + }} + buttonColor="danger" + onCancel={() => { + setShowDeletionConfirmationModal(false); + }} + /> + )} + + ); + + return listComponent; +} diff --git a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx index 3d8aa26af22af..c4e54995b5979 100644 --- a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx @@ -191,11 +191,12 @@ export function createSearchBar({ core, storage, data, usageCollection }: Statef onSaved={defaultOnSavedQueryUpdated(props, setSavedQuery)} iconType={props.iconType} nonKqlMode={props.nonKqlMode} - nonKqlModeHelpText={props.nonKqlModeHelpText} customSubmitButton={props.customSubmitButton} isClearable={props.isClearable} placeholder={props.placeholder} {...overrideDefaultBehaviors(props)} + dataViewPickerComponentProps={props.dataViewPickerComponentProps} + displayStyle={props.displayStyle} /> ); diff --git a/src/plugins/unified_search/public/search_bar/index.tsx b/src/plugins/unified_search/public/search_bar/index.tsx index f8c9de7ec7d87..40421a50a5fe2 100644 --- a/src/plugins/unified_search/public/search_bar/index.tsx +++ b/src/plugins/unified_search/public/search_bar/index.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { injectI18n } from '@kbn/i18n-react'; import { withKibana } from '@kbn/kibana-react-plugin/public'; import type { SearchBarProps } from './search_bar'; +import '../index.scss'; const Fallback = () =>
; diff --git a/src/plugins/unified_search/public/search_bar/search_bar.styles.ts b/src/plugins/unified_search/public/search_bar/search_bar.styles.ts new file mode 100644 index 0000000000000..1072a684eeaad --- /dev/null +++ b/src/plugins/unified_search/public/search_bar/search_bar.styles.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UseEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; + +export const searchBarStyles = ({ euiTheme }: UseEuiTheme) => { + return { + uniSearchBar: css` + padding: ${euiTheme.size.s}; + `, + detached: css` + border-bottom: ${euiTheme.border.thin}; + `, + inPage: css` + padding: 0; + `, + }; +}; diff --git a/src/plugins/unified_search/public/search_bar/search_bar.test.tsx b/src/plugins/unified_search/public/search_bar/search_bar.test.tsx index 14310b69809e0..fe5e03ab7fb37 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.test.tsx +++ b/src/plugins/unified_search/public/search_bar/search_bar.test.tsx @@ -16,24 +16,21 @@ import { coreMock } from '@kbn/core/public/mocks'; const startMock = coreMock.createStart(); import { mount } from 'enzyme'; -import { IIndexPattern } from '@kbn/data-plugin/public'; +import { DataView } from '@kbn/data-views-plugin/public'; +import { EuiThemeProvider } from '@elastic/eui'; const mockTimeHistory = { get: () => { return []; }, + add: jest.fn(), + get$: () => { + return { + pipe: () => {}, + }; + }, }; -jest.mock('../filter_bar', () => { - return { - FilterBar: () =>
, - }; -}); - -jest.mock('../query_string_input/query_bar_top_row', () => { - return () =>
; -}); - const noop = jest.fn(); const createMockWebStorage = () => ({ @@ -66,7 +63,7 @@ const mockIndexPattern = { searchable: true, }, ], -} as IIndexPattern; +} as DataView; const kqlQuery = { query: 'response:200', @@ -88,24 +85,45 @@ function wrapSearchBarInContext(testProps: any) { storage: createMockStorage(), data: { query: { - savedQueries: {}, + savedQueries: { + findSavedQueries: () => + Promise.resolve({ + queries: [ + { + id: 'testwewe', + attributes: { + title: 'Saved query 1', + description: '', + query: { + query: 'category.keyword : "Men\'s Shoes" ', + language: 'kuery', + }, + filters: [], + }, + }, + ], + }), + }, }, }, }; return ( - - - - - + + + + + + + ); } describe('SearchBar', () => { - const SEARCH_BAR_ROOT = '.globalQueryBar'; - const FILTER_BAR = '.filterBar'; - const QUERY_BAR = '.queryBar'; + const SEARCH_BAR_ROOT = '.uniSearchBar'; + const FILTER_BAR = '[data-test-subj="unifiedFilterBar"]'; + const QUERY_BAR = '.kbnQueryBar'; + const QUERY_INPUT = '[data-test-subj="unifiedQueryInput"]'; beforeEach(() => { jest.clearAllMocks(); @@ -118,22 +136,9 @@ describe('SearchBar', () => { }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(1); - }); - - it('Should render empty when timepicker is off and no options provided', () => { - const component = mount( - wrapSearchBarInContext({ - indexPatterns: [mockIndexPattern], - showDatePicker: false, - }) - ); - - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT)).toBeTruthy(); + expect(component.find(FILTER_BAR).length).toBeFalsy(); + expect(component.find(QUERY_BAR).length).toBeTruthy(); }); it('Should render filter bar, when required fields are provided', () => { @@ -141,14 +146,16 @@ describe('SearchBar', () => { wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], showDatePicker: false, + showQueryInput: true, + showFilterBar: true, onFiltersUpdated: noop, filters: [], }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(1); - expect(component.find(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT)).toBeTruthy(); + expect(component.find(FILTER_BAR).length).toBeTruthy(); + expect(component.find(QUERY_BAR).length).toBeTruthy(); }); it('Should NOT render filter bar, if disabled', () => { @@ -162,9 +169,9 @@ describe('SearchBar', () => { }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT)).toBeTruthy(); + expect(component.find(FILTER_BAR).length).toBeFalsy(); + expect(component.find(QUERY_BAR).length).toBeTruthy(); }); it('Should render query bar, when required fields are provided', () => { @@ -177,12 +184,12 @@ describe('SearchBar', () => { }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(1); + expect(component.find(SEARCH_BAR_ROOT)).toBeTruthy(); + expect(component.find(FILTER_BAR).length).toBeFalsy(); + expect(component.find(QUERY_BAR).length).toBeTruthy(); }); - it('Should NOT render query bar, if disabled', () => { + it('Should NOT render the input query input, if disabled', () => { const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], @@ -190,12 +197,13 @@ describe('SearchBar', () => { onQuerySubmit: noop, query: kqlQuery, showQueryBar: false, + showQueryInput: false, }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT)).toBeTruthy(); + expect(component.find(FILTER_BAR).length).toBeFalsy(); + expect(component.find(QUERY_INPUT).length).toBeFalsy(); }); it('Should render query bar and filter bar', () => { @@ -203,6 +211,7 @@ describe('SearchBar', () => { wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], screenTitle: 'test screen', + showQueryInput: true, onQuerySubmit: noop, query: kqlQuery, filters: [], @@ -210,8 +219,9 @@ describe('SearchBar', () => { }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(1); - expect(component.find(QUERY_BAR).length).toBe(1); + expect(component.find(SEARCH_BAR_ROOT)).toBeTruthy(); + expect(component.find(FILTER_BAR).length).toBeTruthy(); + expect(component.find(QUERY_BAR).length).toBeTruthy(); + expect(component.find(QUERY_INPUT).length).toBeTruthy(); }); }); diff --git a/src/plugins/unified_search/public/search_bar/search_bar.tsx b/src/plugins/unified_search/public/search_bar/search_bar.tsx index c829ab66bb60a..a684e5ba928a8 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/search_bar.tsx @@ -11,22 +11,25 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import classNames from 'classnames'; import React, { Component } from 'react'; import { get, isEqual } from 'lodash'; -import { EuiIconProps } from '@elastic/eui'; +import { EuiIconProps, withEuiTheme, WithEuiThemeProps } from '@elastic/eui'; import memoizeOne from 'memoize-one'; import { METRIC_TYPE } from '@kbn/analytics'; import { Query, Filter } from '@kbn/es-query'; import { withKibana, KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; - import type { TimeHistoryContract, SavedQuery } from '@kbn/data-plugin/public'; import type { SavedQueryAttributes } from '@kbn/data-plugin/common'; import { IDataPluginServices } from '@kbn/data-plugin/public'; import { TimeRange } from '@kbn/data-plugin/common'; import { DataView } from '@kbn/data-views-plugin/public'; -import { FilterBar } from '../filter_bar'; -import QueryBarTopRow from '../query_string_input/query_bar_top_row'; + import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form'; -import { SavedQueryManagementComponent } from '../saved_query_management'; +import { SavedQueryManagementList } from '../saved_query_management'; +import { QueryBarMenu } from '../query_string_input/query_bar_menu'; +import type { DataViewPickerProps } from '../dataview_picker'; +import QueryBarTopRow from '../query_string_input/query_bar_top_row'; +import { FilterBar, FilterItems } from '../filter_bar'; +import { searchBarStyles } from './search_bar.styles'; export interface SearchBarInjectedDeps { kibana: KibanaReactContextValue; @@ -77,19 +80,20 @@ export interface SearchBarOwnProps { isClearable?: boolean; iconType?: EuiIconProps['type']; nonKqlMode?: 'lucene' | 'text'; - nonKqlModeHelpText?: string; - // defines padding; use 'inPage' to avoid extra padding; use 'detached' if the searchBar appears at the very top of the view, without any wrapper + // defines padding and border; use 'inPage' to avoid any padding or border; + // use 'detached' if the searchBar appears at the very top of the view, without any wrapper displayStyle?: 'inPage' | 'detached'; // super update button background fill control fillSubmitButton?: boolean; + dataViewPickerComponentProps?: DataViewPickerProps; + showSubmitButton?: boolean; } export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; interface State { isFiltersVisible: boolean; - showSaveQueryModal: boolean; - showSaveNewQueryModal: boolean; + openQueryBarMenu: boolean; showSavedQueryPopover: boolean; currentProps?: SearchBarProps; query?: Query; @@ -97,11 +101,12 @@ interface State { dateRangeTo: string; } -class SearchBarUI extends Component { +class SearchBarUI extends Component { public static defaultProps = { showQueryBar: true, showFilterBar: true, showDatePicker: true, + showSubmitButton: true, showAutoRefreshOnly: false, }; @@ -168,8 +173,7 @@ class SearchBarUI extends Component { */ public state = { isFiltersVisible: true, - showSaveQueryModal: false, - showSaveNewQueryModal: false, + openQueryBarMenu: false, showSavedQueryPopover: false, currentProps: this.props, query: this.props.query ? { ...this.props.query } : undefined, @@ -193,13 +197,6 @@ class SearchBarUI extends Component { this.renderSavedQueryManagement.clear(); } - private shouldRenderQueryBar() { - const showDatePicker = this.props.showDatePicker || this.props.showAutoRefreshOnly; - const showQueryInput = - this.props.showQueryInput && this.props.indexPatterns && this.state.query; - return this.props.showQueryBar && (showDatePicker || showQueryInput); - } - private shouldRenderFilterBar() { return ( this.props.showFilterBar && @@ -266,11 +263,6 @@ class SearchBarUI extends Component { `Your query "${response.attributes.title}" was saved` ); - this.setState({ - showSaveQueryModal: false, - showSaveNewQueryModal: false, - }); - if (this.props.onSaved) { this.props.onSaved(response); } @@ -282,18 +274,6 @@ class SearchBarUI extends Component { } }; - public onInitiateSave = () => { - this.setState({ - showSaveQueryModal: true, - }); - }; - - public onInitiateSaveNew = () => { - this.setState({ - showSaveNewQueryModal: true, - }); - }; - public onQueryBarChange = (queryAndDateRange: { dateRange: TimeRange; query?: Query }) => { this.setState({ query: queryAndDateRange.query, @@ -305,6 +285,12 @@ class SearchBarUI extends Component { } }; + public toggleFilterBarMenuPopover = (value: boolean) => { + this.setState({ + openQueryBarMenu: value, + }); + }; + public onQueryBarSubmit = (queryAndDateRange: { dateRange?: TimeRange; query?: Query }) => { this.setState( { @@ -349,12 +335,104 @@ class SearchBarUI extends Component { } }; + private shouldShowDatePickerAsBadge() { + return this.shouldRenderFilterBar() && !this.props.showQueryInput; + } + public render() { + const { theme } = this.props; + const styles = searchBarStyles(theme); + const cssStyles = [ + styles.uniSearchBar, + this.props.displayStyle && styles[this.props.displayStyle], + ]; + + const classes = classNames('uniSearchBar', { + [`uniSearchBar--${this.props.displayStyle}`]: this.props.displayStyle, + }); + const timeRangeForSuggestionsOverride = this.props.showDatePicker ? undefined : false; - let queryBar; - if (this.shouldRenderQueryBar()) { - queryBar = ( + const saveAsNewQueryFormComponent = ( + this.onSave(savedQueryMeta, true)} + onClose={() => this.setState({ openQueryBarMenu: false })} + showFilterOption={this.props.showFilterBar} + showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} + /> + ); + + const saveQueryFormComponent = ( + this.setState({ openQueryBarMenu: false })} + showFilterOption={this.props.showFilterBar} + showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} + /> + ); + + const queryBarMenu = ( + + ); + + let filterBar; + if (this.shouldRenderFilterBar()) { + filterBar = this.shouldShowDatePickerAsBadge() ? ( + + ) : ( + + ); + } + + return ( +
{ indexPatterns={this.props.indexPatterns} isLoading={this.props.isLoading} fillSubmitButton={this.props.fillSubmitButton || false} - prepend={ - this.props.showFilterBar && this.state.query - ? this.renderSavedQueryManagement( - this.props.onClearSavedQuery, - this.props.showSaveQuery, - this.props.savedQuery - ) - : undefined - } + prepend={this.props.showFilterBar || this.props.showQueryInput ? queryBarMenu : undefined} showDatePicker={this.props.showDatePicker} dateRangeFrom={this.state.dateRangeFrom} dateRangeTo={this.state.dateRangeTo} @@ -379,6 +449,7 @@ class SearchBarUI extends Component { refreshInterval={this.props.refreshInterval} showAutoRefreshOnly={this.props.showAutoRefreshOnly} showQueryInput={this.props.showQueryInput} + showAddFilter={this.props.showFilterBar} onRefresh={this.props.onRefresh} onRefreshChange={this.props.onRefreshChange} onChange={this.onQueryBarChange} @@ -386,70 +457,30 @@ class SearchBarUI extends Component { customSubmitButton={ this.props.customSubmitButton ? this.props.customSubmitButton : undefined } + showSubmitButton={this.props.showSubmitButton} dataTestSubj={this.props.dataTestSubj} indicateNoData={this.props.indicateNoData} placeholder={this.props.placeholder} isClearable={this.props.isClearable} iconType={this.props.iconType} nonKqlMode={this.props.nonKqlMode} - nonKqlModeHelpText={this.props.nonKqlModeHelpText} timeRangeForSuggestionsOverride={timeRangeForSuggestionsOverride} + filters={this.props.filters!} + onFiltersUpdated={this.props.onFiltersUpdated} + dataViewPickerComponentProps={this.props.dataViewPickerComponentProps} + showDatePickerAsBadge={this.shouldShowDatePickerAsBadge()} + filterBar={filterBar} /> - ); - } - - let filterBar; - if (this.shouldRenderFilterBar()) { - const filterGroupClasses = classNames('globalFilterGroup__wrapper', { - 'globalFilterGroup__wrapper-isVisible': this.state.isFiltersVisible, - }); - - filterBar = ( -
- -
- ); - } - - const globalQueryBarClasses = classNames('globalQueryBar', { - 'globalQueryBar--inPage': this.props.displayStyle === 'inPage', - }); - - return ( -
- {queryBar} - {filterBar} - - {this.state.showSaveQueryModal ? ( - this.setState({ showSaveQueryModal: false })} - showFilterOption={this.props.showFilterBar} - showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} - /> - ) : null} - {this.state.showSaveNewQueryModal ? ( - this.onSave(savedQueryMeta, true)} - onClose={() => this.setState({ showSaveNewQueryModal: false })} - showFilterOption={this.props.showFilterBar} - showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} - /> - ) : null}
); } + private hasFiltersOrQuery() { + const hasFilters = Boolean(this.props.filters && this.props.filters.length > 0); + const hasQuery = Boolean(this.state.query && this.state.query.query); + return hasFilters || hasQuery; + } + private renderSavedQueryManagement = memoizeOne( ( onClearSavedQuery: SearchBarOwnProps['onClearSavedQuery'], @@ -457,14 +488,14 @@ class SearchBarUI extends Component { savedQuery: SearchBarOwnProps['savedQuery'] ) => { const savedQueryManagement = onClearSavedQuery && ( - this.setState({ openQueryBarMenu: false })} + hasFiltersOrQuery={this.hasFiltersOrQuery()} /> ); @@ -475,4 +506,4 @@ class SearchBarUI extends Component { // Needed for React.lazy // eslint-disable-next-line import/no-default-export -export default injectI18n(withKibana(SearchBarUI)); +export default injectI18n(withEuiTheme(withKibana(SearchBarUI))); diff --git a/src/plugins/unified_search/public/typeahead/_suggestion.scss b/src/plugins/unified_search/public/typeahead/_suggestion.scss index e466a52e7fc10..a59e53a102d6c 100644 --- a/src/plugins/unified_search/public/typeahead/_suggestion.scss +++ b/src/plugins/unified_search/public/typeahead/_suggestion.scss @@ -15,12 +15,16 @@ $kbnTypeaheadTypes: ( @include euiBottomShadowFlat; border-top-left-radius: $euiBorderRadius; border-top-right-radius: $euiBorderRadius; + // Clips the shadow so it doesn't show above the input (below) + clip-path: polygon(-50px -50px, calc(100% + 50px) -50px, calc(100% + 50px) 100%, -50px 100%); } .kbnTypeahead__popover--bottom { @include euiBottomShadow; border-bottom-left-radius: $euiBorderRadius; border-bottom-right-radius: $euiBorderRadius; + // Clips the shadow so it doesn't show above the input (top) + clip-path: polygon(-50px 1px, calc(100% + 50px) 1px, calc(100% + 50px) calc(100% + 50px), -50px calc(100% + 50px)); } .kbnTypeahead { @@ -59,7 +63,6 @@ $kbnTypeaheadTypes: ( .kbnTypeahead__item:first-child { border-bottom: none; - border-radius: $euiBorderRadius $euiBorderRadius 0 0; } .kbnTypeahead__item.active { diff --git a/src/plugins/unified_search/public/typeahead/suggestions_component.tsx b/src/plugins/unified_search/public/typeahead/suggestions_component.tsx index 75e446cf2d6e8..ebeddfaaff81f 100644 --- a/src/plugins/unified_search/public/typeahead/suggestions_component.tsx +++ b/src/plugins/unified_search/public/typeahead/suggestions_component.tsx @@ -9,8 +9,7 @@ import React, { PureComponent, ReactNode } from 'react'; import { isEmpty } from 'lodash'; import classNames from 'classnames'; - -import styled from 'styled-components'; +import { css } from '@emotion/react'; import useRafState from 'react-use/lib/useRafState'; import { QuerySuggestion } from '../autocomplete'; @@ -146,15 +145,6 @@ export default class SuggestionsComponent extends PureComponent ` - position: absolute; - z-index: 4001; - left: ${props.left}px; - width: ${props.width}px; - ${props.verticalListPosition}`} -`; - const ResizableSuggestionsListDiv: React.FC<{ inputContainer: HTMLElement; suggestionsSize?: SuggestionsListSize; @@ -174,12 +164,16 @@ const ResizableSuggestionsListDiv: React.FC<{ ? `top: ${pageYOffset + containerRect.bottom - SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET}px;` : `bottom: ${documentHeight - (pageYOffset + containerRect.top)}px;`; + const divPosition = css` + position: absolute; + z-index: 4001; + left: ${containerRect.left}px; + width: ${containerRect.width}px; + ${verticalListPosition} + `; + return ( - +
- +
); }); diff --git a/src/plugins/vis_type_markdown/public/markdown_vis.ts b/src/plugins/vis_type_markdown/public/markdown_vis.ts index 7fcf9fb6311e6..a4b4010064e78 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/plugins/vis_type_markdown/public/markdown_vis.ts @@ -58,6 +58,8 @@ export const markdownVisDefinition: VisTypeDefinition = { options: { showTimePicker: false, showFilterBar: false, + showQueryBar: true, + showQueryInput: false, }, inspectorAdapters: {}, }; diff --git a/src/plugins/vis_types/timelion/public/timelion_vis_type.tsx b/src/plugins/vis_types/timelion/public/timelion_vis_type.tsx index f32a485ac2565..f8d7415f6aefe 100644 --- a/src/plugins/vis_types/timelion/public/timelion_vis_type.tsx +++ b/src/plugins/vis_types/timelion/public/timelion_vis_type.tsx @@ -66,8 +66,9 @@ export function getTimelionVisDefinition(dependencies: TimelionVisDependencies) }, options: { showIndexSelection: false, - showQueryBar: false, + showQueryBar: true, showFilterBar: false, + showQueryInput: false, }, requiresSearch: true, }; diff --git a/src/plugins/visualizations/public/vis_types/base_vis_type.ts b/src/plugins/visualizations/public/vis_types/base_vis_type.ts index 80295e5af2e40..bb197e219f439 100644 --- a/src/plugins/visualizations/public/vis_types/base_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/base_vis_type.ts @@ -18,6 +18,7 @@ const defaultOptions: VisTypeOptions = { showQueryBar: true, showFilterBar: true, showIndexSelection: true, + showQueryInput: true, hierarchicalData: false, // we should get rid of this i guess ? }; diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 0e7e44b6ea38e..383a238621e1e 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -20,6 +20,7 @@ export interface VisTypeOptions { showQueryBar: boolean; showFilterBar: boolean; showIndexSelection: boolean; + showQueryInput: boolean; hierarchicalData: boolean; } diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx index a6c1710afbed8..e42ee1d0cd6c0 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx @@ -161,7 +161,8 @@ const TopNav = ({ return vis.type.options.showTimePicker && hasTimeField; }; const showFilterBar = vis.type.options.showFilterBar; - const showQueryInput = vis.type.requiresSearch && vis.type.options.showQueryBar; + const showQueryInput = + vis.type.requiresSearch && vis.type.options.showQueryBar && vis.type.options.showQueryInput; useEffect(() => { return () => { diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts index 99dbf548e6f44..19f117ec18cc8 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts @@ -2470,6 +2470,31 @@ describe('migration visualization', () => { }); }); + it('should not apply search source migrations within visualization when searchSourceJSON is not an object', () => { + const visualizationDoc = { + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '5', + }, + }, + } as SavedObjectUnsanitizedDoc; + + const versionToTest = '1.2.4'; + const visMigrations = getAllMigrations({ + [versionToTest]: (state) => ({ ...state, migrated: true }), + }); + + expect( + visMigrations[versionToTest](visualizationDoc, {} as SavedObjectMigrationContext) + ).toEqual({ + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '5', + }, + }, + }); + }); + describe('8.1.0 pie - labels and addLegend migration', () => { const getDoc = (addLegend: boolean, lastLevel: boolean = false) => ({ attributes: { diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts index 4b729afa62307..d236ad83c853a 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts @@ -11,7 +11,11 @@ import type { SavedObjectMigrationFn, SavedObjectMigrationMap } from '@kbn/core/ import { mergeSavedObjectMigrationMaps } from '@kbn/core/server'; import { MigrateFunctionsObject, MigrateFunction } from '@kbn/kibana-utils-plugin/common'; -import { DEFAULT_QUERY_LANGUAGE, SerializedSearchSourceFields } from '@kbn/data-plugin/common'; +import { + DEFAULT_QUERY_LANGUAGE, + isSerializedSearchSource, + SerializedSearchSourceFields, +} from '@kbn/data-plugin/common'; import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; import { commonAddSupportOfDualIndexSelectionModeInTSVB, @@ -1215,27 +1219,31 @@ const visualizationSavedObjectTypeMigrations = { /** * This creates a migration map that applies search source migrations to legacy visualization SOs */ -const getVisualizationSearchSourceMigrations = (searchSourceMigrations: MigrateFunctionsObject) => +const getVisualizationSearchSourceMigrations = ( + searchSourceMigrations: MigrateFunctionsObject +): MigrateFunctionsObject => mapValues( searchSourceMigrations, (migrate: MigrateFunction): MigrateFunction => (state) => { - const _state = state as unknown as { attributes: VisualizationSavedObjectAttributes }; - - const parsedSearchSourceJSON = _state.attributes.kibanaSavedObjectMeta.searchSourceJSON; - - if (!parsedSearchSourceJSON) return _state; - - return { - ..._state, - attributes: { - ..._state.attributes, - kibanaSavedObjectMeta: { - ..._state.attributes.kibanaSavedObjectMeta, - searchSourceJSON: JSON.stringify(migrate(JSON.parse(parsedSearchSourceJSON))), + const _state = state as { attributes: VisualizationSavedObjectAttributes }; + + const parsedSearchSourceJSON = JSON.parse( + _state.attributes.kibanaSavedObjectMeta.searchSourceJSON + ); + if (isSerializedSearchSource(parsedSearchSourceJSON)) { + return { + ..._state, + attributes: { + ..._state.attributes, + kibanaSavedObjectMeta: { + ..._state.attributes.kibanaSavedObjectMeta, + searchSourceJSON: JSON.stringify(migrate(parsedSearchSourceJSON)), + }, }, - }, - }; + }; + } + return _state; } ); @@ -1244,7 +1252,5 @@ export const getAllMigrations = ( ): SavedObjectMigrationMap => mergeSavedObjectMigrationMaps( visualizationSavedObjectTypeMigrations, - getVisualizationSearchSourceMigrations( - searchSourceMigrations - ) as unknown as SavedObjectMigrationMap + getVisualizationSearchSourceMigrations(searchSourceMigrations) as SavedObjectMigrationMap ); diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index 867e146e64ca3..5a3e881b86471 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -11,6 +11,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'discover', 'header', 'share', 'timePicker']); const a11y = getService('a11y'); + const savedQueryManagementComponent = getService('savedQueryManagementComponent'); const inspector = getService('inspector'); const testSubjects = getService('testSubjects'); const TEST_COLUMN_NAMES = ['dayOfWeek', 'DestWeather']; @@ -93,11 +94,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('a11y test on saved queries list panel', async () => { + await savedQueryManagementComponent.loadSavedQuery('test'); await PageObjects.discover.clickSavedQueriesPopOver(); - await testSubjects.moveMouseTo( - 'saved-query-list-item load-saved-query-test-button saved-query-list-item-selected saved-query-list-item-selected' - ); - await testSubjects.find('delete-saved-query-test-button'); + await testSubjects.click('saved-query-management-load-button'); + await savedQueryManagementComponent.deleteSavedQuery('test'); await a11y.testAppSnapshot(); }); }); diff --git a/test/accessibility/apps/filter_panel.ts b/test/accessibility/apps/filter_panel.ts index deb1e9512cd81..b479c62f48975 100644 --- a/test/accessibility/apps/filter_panel.ts +++ b/test/accessibility/apps/filter_panel.ts @@ -43,38 +43,47 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // the following tests filter panel options which changes UI it('a11y test on filter panel options panel', async () => { await filterBar.addFilter('DestCountry', 'is', 'AU'); - await testSubjects.click('showFilterActions'); + await testSubjects.click('showQueryBarMenu'); await a11y.testAppSnapshot(); }); it('a11y test on disable all filter options view', async () => { - await testSubjects.click('disableAllFilters'); + await testSubjects.click('filter-sets-applyToAllFilters'); + await testSubjects.click('filter-sets-disableAllFilters'); await a11y.testAppSnapshot(); }); - it('a11y test on pin filters view', async () => { - await testSubjects.click('showFilterActions'); - await testSubjects.click('enableAllFilters'); - await testSubjects.click('showFilterActions'); - await testSubjects.click('pinAllFilters'); + it('a11y test on enable all filters view', async () => { + await testSubjects.click('showQueryBarMenu'); + await testSubjects.click('filter-sets-applyToAllFilters'); + await testSubjects.click('filter-sets-enableAllFilters'); + await a11y.testAppSnapshot(); + }); + + it('a11y test on pin all filters view', async () => { + await testSubjects.click('showQueryBarMenu'); + await testSubjects.click('filter-sets-applyToAllFilters'); + await testSubjects.click('filter-sets-pinAllFilters'); await a11y.testAppSnapshot(); }); it('a11y test on unpin all filters view', async () => { - await testSubjects.click('showFilterActions'); - await testSubjects.click('unpinAllFilters'); + await testSubjects.click('showQueryBarMenu'); + await testSubjects.click('filter-sets-applyToAllFilters'); + await testSubjects.click('filter-sets-unpinAllFilters'); await a11y.testAppSnapshot(); }); it('a11y test on invert inclusion of all filters view', async () => { - await testSubjects.click('showFilterActions'); - await testSubjects.click('invertInclusionAllFilters'); + await testSubjects.click('showQueryBarMenu'); + await testSubjects.click('filter-sets-applyToAllFilters'); + await testSubjects.click('filter-sets-invertAllFilters'); await a11y.testAppSnapshot(); }); it('a11y test on remove all filtes view', async () => { - await testSubjects.click('showFilterActions'); - await testSubjects.click('removeAllFilters'); + await testSubjects.click('showQueryBarMenu'); + await testSubjects.click('filter-sets-removeAllFilters'); await a11y.testAppSnapshot(); }); }); diff --git a/test/accessibility/config.ts b/test/accessibility/config.ts index 59194fcb67826..9ed89694db5d8 100644 --- a/test/accessibility/config.ts +++ b/test/accessibility/config.ts @@ -11,7 +11,7 @@ import { services } from './services'; import { pageObjects } from './page_objects'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { ...functionalConfig.getAll(), diff --git a/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/public/custom_shipper.ts b/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/public/custom_shipper.ts index 31c9302e5ece8..97bf37749c256 100644 --- a/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/public/custom_shipper.ts +++ b/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/public/custom_shipper.ts @@ -7,17 +7,25 @@ */ import { Subject } from 'rxjs'; +import type { AnalyticsClientInitContext } from '@kbn/analytics-client'; import type { Event, IShipper } from '@kbn/core/public'; export class CustomShipper implements IShipper { public static shipperName = 'FTR-helpers-shipper'; - constructor(private readonly events$: Subject) {} + constructor( + private readonly events$: Subject, + private readonly initContext: AnalyticsClientInitContext + ) {} public reportEvents(events: Event[]) { + this.initContext.logger.info( + `Reporting ${events.length} events to ${CustomShipper.shipperName}: ${JSON.stringify(events)}` + ); events.forEach((event) => { this.events$.next(event); }); } optIn(isOptedIn: boolean) {} + shutdown() {} } diff --git a/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/server/custom_shipper.ts b/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/server/custom_shipper.ts index 0feb1f2d13e7c..c76f30c94572e 100644 --- a/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/server/custom_shipper.ts +++ b/test/analytics/__fixtures__/plugins/analytics_ftr_helpers/server/custom_shipper.ts @@ -7,17 +7,25 @@ */ import { Subject } from 'rxjs'; +import type { AnalyticsClientInitContext } from '@kbn/analytics-client'; import type { IShipper, Event } from '@kbn/core/server'; export class CustomShipper implements IShipper { public static shipperName = 'FTR-helpers-shipper'; - constructor(private readonly events$: Subject) {} + constructor( + private readonly events$: Subject, + private readonly initContext: AnalyticsClientInitContext + ) {} public reportEvents(events: Event[]) { + this.initContext.logger.info( + `Reporting ${events.length} events to ${CustomShipper.shipperName}: ${JSON.stringify(events)}` + ); events.forEach((event) => { this.events$.next(event); }); } optIn(isOptedIn: boolean) {} + shutdown() {} } diff --git a/test/analytics/__fixtures__/plugins/analytics_plugin_a/public/custom_shipper.ts b/test/analytics/__fixtures__/plugins/analytics_plugin_a/public/custom_shipper.ts index e94e34126c7d4..075f5bcdb99a1 100644 --- a/test/analytics/__fixtures__/plugins/analytics_plugin_a/public/custom_shipper.ts +++ b/test/analytics/__fixtures__/plugins/analytics_plugin_a/public/custom_shipper.ts @@ -40,4 +40,5 @@ export class CustomShipper implements IShipper { extendContext(newContext: EventContext) { this.actions$.next({ action: 'extendContext', meta: newContext }); } + shutdown() {} } diff --git a/test/analytics/__fixtures__/plugins/analytics_plugin_a/server/custom_shipper.ts b/test/analytics/__fixtures__/plugins/analytics_plugin_a/server/custom_shipper.ts index 7d0b2bb1b6db2..dd3b99a154968 100644 --- a/test/analytics/__fixtures__/plugins/analytics_plugin_a/server/custom_shipper.ts +++ b/test/analytics/__fixtures__/plugins/analytics_plugin_a/server/custom_shipper.ts @@ -40,4 +40,5 @@ export class CustomShipper implements IShipper { extendContext(newContext: EventContext) { this.actions$.next({ action: 'extendContext', meta: newContext }); } + shutdown() {} } diff --git a/test/analytics/config.ts b/test/analytics/config.ts index 1ecac5af0d01a..ecb9792b0dff1 100644 --- a/test/analytics/config.ts +++ b/test/analytics/config.ts @@ -19,7 +19,7 @@ import { services } from './services'; */ export default async function ({ readConfigFile }: FtrConfigProviderContext) { const commonConfig = await readConfigFile(require.resolve('../common/config')); - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { testFiles: [require.resolve('./tests')], @@ -34,7 +34,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...functionalConfig.get('kbnTestServer'), serverArgs: [ ...functionalConfig.get('kbnTestServer.serverArgs'), - // Disabling telemetry so it doesn't call opt-in before the tests run. + // Disabling telemetry, so it doesn't call opt-in before the tests run. '--telemetry.enabled=false', `--plugin-path=${path.resolve(__dirname, './__fixtures__/plugins/analytics_plugin_a')}`, `--plugin-path=${path.resolve(__dirname, './__fixtures__/plugins/analytics_ftr_helpers')}`, diff --git a/test/analytics/services/kibana_ebt.ts b/test/analytics/services/kibana_ebt.ts index fd64cbbbc0105..281794e899a3c 100644 --- a/test/analytics/services/kibana_ebt.ts +++ b/test/analytics/services/kibana_ebt.ts @@ -12,24 +12,27 @@ import '@kbn/analytics-ftr-helpers-plugin/public/types'; export function KibanaEBTServerProvider({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const setOptIn = async (optIn: boolean) => { + await supertest + .post(`/internal/analytics_ftr_helpers/opt_in`) + .set('kbn-xsrf', 'xxx') + .query({ consent: optIn }) + .expect(200); + }; + return { /** * Change the opt-in state of the Kibana EBT client. * @param optIn `true` to opt-in, `false` to opt-out. */ - setOptIn: async (optIn: boolean) => { - await supertest - .post(`/internal/analytics_ftr_helpers/opt_in`) - .set('kbn-xsrf', 'xxx') - .query({ consent: optIn }) - .expect(200); - }, + setOptIn, /** * Returns the last events of the specified types. * @param numberOfEvents - number of events to return * @param eventTypes (Optional) array of event types to return */ getLastEvents: async (takeNumberOfEvents: number, eventTypes: string[] = []) => { + await setOptIn(true); const resp = await supertest .get(`/internal/analytics_ftr_helpers/events`) .query({ takeNumberOfEvents, eventTypes: JSON.stringify(eventTypes) }) @@ -45,6 +48,10 @@ export function KibanaEBTUIProvider({ getService, getPageObjects }: FtrProviderC const { common } = getPageObjects(['common']); const browser = getService('browser'); + const setOptIn = async (optIn: boolean) => { + await browser.execute((isOptIn) => window.__analytics_ftr_helpers__.setOptIn(isOptIn), optIn); + }; + return { /** * Change the opt-in state of the Kibana EBT client. @@ -52,7 +59,7 @@ export function KibanaEBTUIProvider({ getService, getPageObjects }: FtrProviderC */ setOptIn: async (optIn: boolean) => { await common.navigateToApp('home'); - await browser.execute((isOptIn) => window.__analytics_ftr_helpers__.setOptIn(isOptIn), optIn); + await setOptIn(optIn); }, /** * Returns the last events of the specified types. @@ -60,6 +67,7 @@ export function KibanaEBTUIProvider({ getService, getPageObjects }: FtrProviderC * @param eventTypes (Optional) array of event types to return */ getLastEvents: async (numberOfEvents: number, eventTypes: string[] = []) => { + await setOptIn(true); const events = await browser.execute( ({ eventTypes: _eventTypes, numberOfEvents: _numberOfEvents }) => window.__analytics_ftr_helpers__.getLastEvents(_numberOfEvents, _eventTypes), diff --git a/test/analytics/tests/analytics_from_the_browser.ts b/test/analytics/tests/analytics_from_the_browser.ts index 7acabf2112c5d..c05492fe30961 100644 --- a/test/analytics/tests/analytics_from_the_browser.ts +++ b/test/analytics/tests/analytics_from_the_browser.ts @@ -72,6 +72,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(context).to.have.property('user_agent'); expect(context.user_agent).to.be.a('string'); + // Some context providers emit very early. We are OK with that. + const initialContext = actions[2].meta[0].context; + const reportEventContext = actions[2].meta[1].context; expect(reportEventContext).to.have.property('user_agent'); expect(reportEventContext.user_agent).to.be.a('string'); @@ -85,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { { timestamp: actions[2].meta[0].timestamp, event_type: 'test-plugin-lifecycle', - context: {}, + context: initialContext, properties: { plugin: 'analyticsPluginA', step: 'setup' }, }, { @@ -103,7 +106,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { { timestamp: actions[2].meta[0].timestamp, event_type: 'test-plugin-lifecycle', - context: {}, + context: initialContext, properties: { plugin: 'analyticsPluginA', step: 'setup' }, }, { diff --git a/test/analytics/tests/analytics_from_the_server.ts b/test/analytics/tests/analytics_from_the_server.ts index e5e3573b20fcd..820f7e51adc96 100644 --- a/test/analytics/tests/analytics_from_the_server.ts +++ b/test/analytics/tests/analytics_from_the_server.ts @@ -63,11 +63,19 @@ export default function ({ getService }: FtrProviderContext) { await ebtServerHelper.setOptIn(true); const actions = await getActions(3); + // Validating the remote PID because that's the only field that it's added by the FTR plugin. const context = actions[1].meta; expect(context).to.have.property('pid'); expect(context.pid).to.be.a('number'); + // Some context providers emit very early. We are OK with that. + const initialContext = actions[2].meta[0].context; + + const reportEventContext = actions[2].meta[1].context; + expect(context).to.have.property('pid'); + expect(context.pid).to.be.a('number'); + expect(actions).to.eql([ { action: 'optIn', meta: true }, { action: 'extendContext', meta: context }, @@ -77,13 +85,13 @@ export default function ({ getService }: FtrProviderContext) { { timestamp: actions[2].meta[0].timestamp, event_type: 'test-plugin-lifecycle', - context: {}, + context: initialContext, properties: { plugin: 'analyticsPluginA', step: 'setup' }, }, { timestamp: actions[2].meta[1].timestamp, event_type: 'test-plugin-lifecycle', - context, + context: reportEventContext, properties: { plugin: 'analyticsPluginA', step: 'start' }, }, ], @@ -96,13 +104,13 @@ export default function ({ getService }: FtrProviderContext) { { timestamp: actions[2].meta[0].timestamp, event_type: 'test-plugin-lifecycle', - context: {}, + context: initialContext, properties: { plugin: 'analyticsPluginA', step: 'setup' }, }, { timestamp: actions[2].meta[1].timestamp, event_type: 'test-plugin-lifecycle', - context, + context: reportEventContext, properties: { plugin: 'analyticsPluginA', step: 'start' }, }, ]); diff --git a/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts b/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts new file mode 100644 index 0000000000000..58d8de723639d --- /dev/null +++ b/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { Event } from '@kbn/core/public'; +import { FtrProviderContext } from '../../../services'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const deployment = getService('deployment'); + const ebtUIHelper = getService('kibana_ebt_ui'); + const { common } = getPageObjects(['common']); + + describe('Core Context Providers', () => { + let event: Event; + before(async () => { + await common.navigateToApp('home'); + [event] = await ebtUIHelper.getLastEvents(1, ['Loaded Kibana']); // Get the loaded Kibana event + }); + + it('should have the properties provided by the "cluster info" context provider', () => { + expect(event.context).to.have.property('cluster_uuid'); + expect(event.context.cluster_uuid).to.be.a('string'); + expect(event.context).to.have.property('cluster_name'); + expect(event.context.cluster_name).to.be.a('string'); + expect(event.context).to.have.property('cluster_version'); + expect(event.context.cluster_version).to.be.a('string'); + }); + + it('should have the properties provided by the "build info" context provider', () => { + expect(event.context).to.have.property('isDev'); + expect(event.context.isDev).to.be.a('boolean'); + expect(event.context).to.have.property('isDistributable'); + expect(event.context.isDistributable).to.be.a('boolean'); + expect(event.context).to.have.property('version'); + expect(event.context.version).to.be.a('string'); + expect(event.context).to.have.property('branch'); + expect(event.context.branch).to.be.a('string'); + expect(event.context).to.have.property('buildNum'); + expect(event.context.buildNum).to.be.a('number'); + expect(event.context).to.have.property('buildSha'); + expect(event.context.buildSha).to.be.a('string'); + }); + + it('should have the properties provided by the "session-id" context provider', () => { + expect(event.context).to.have.property('session_id'); + expect(event.context.session_id).to.be.a('string'); + }); + + it('should have the properties provided by the "browser info" context provider', () => { + expect(event.context).to.have.property('user_agent'); + expect(event.context.user_agent).to.be.a('string'); + expect(event.context).to.have.property('preferred_language'); + expect(event.context.preferred_language).to.be.a('string'); + expect(event.context).to.have.property('preferred_languages'); + expect(event.context.preferred_languages).to.be.an('array'); + (event.context.preferred_languages as unknown[]).forEach((lang) => + expect(lang).to.be.a('string') + ); + }); + + it('should have the properties provided by the "execution_context" context provider', () => { + expect(event.context).to.have.property('pageName'); + expect(event.context.pageName).to.be.a('string'); + expect(event.context).to.have.property('applicationId'); + expect(event.context.applicationId).to.be.a('string'); + expect(event.context).not.to.have.property('entityId'); // In the Home app it's not available. + expect(event.context).not.to.have.property('page'); // In the Home app it's not available. + }); + + it('should have the properties provided by the "license info" context provider', () => { + expect(event.context).to.have.property('license_id'); + expect(event.context.license_id).to.be.a('string'); + expect(event.context).to.have.property('license_status'); + expect(event.context.license_status).to.be.a('string'); + expect(event.context).to.have.property('license_type'); + expect(event.context.license_type).to.be.a('string'); + }); + + it('should have the properties provided by the "Cloud Deployment ID" context provider', async () => { + if (await deployment.isCloud()) { + expect(event.context).to.have.property('cloudId'); + expect(event.context.cloudId).to.be.a('string'); + } else { + expect(event.context).not.to.have.property('cloudId'); + } + }); + }); +} diff --git a/test/analytics/tests/instrumented_events/from_the_browser/index.ts b/test/analytics/tests/instrumented_events/from_the_browser/index.ts index daf21180d2328..69aff97006d72 100644 --- a/test/analytics/tests/instrumented_events/from_the_browser/index.ts +++ b/test/analytics/tests/instrumented_events/from_the_browser/index.ts @@ -8,13 +8,10 @@ import { FtrProviderContext } from '../../../services'; -export default function ({ getService }: FtrProviderContext) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('from the browser', () => { - beforeEach(async () => { - await getService('kibana_ebt_ui').setOptIn(true); - }); - // Add tests for UI-instrumented events here: - // loadTestFile(require.resolve('./some_event')); + loadTestFile(require.resolve('./loaded_kibana')); + loadTestFile(require.resolve('./core_context_providers')); }); } diff --git a/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts b/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts new file mode 100644 index 0000000000000..c7d3291cb03d4 --- /dev/null +++ b/test/analytics/tests/instrumented_events/from_the_browser/loaded_kibana.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../services'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const ebtUIHelper = getService('kibana_ebt_ui'); + const { common } = getPageObjects(['common']); + const browser = getService('browser'); + + describe('Loaded Kibana', () => { + beforeEach(async () => { + await common.navigateToApp('home'); + }); + + it('should emit the "Loaded Kibana" event', async () => { + const [event] = await ebtUIHelper.getLastEvents(1, ['Loaded Kibana']); + expect(event.event_type).to.eql('Loaded Kibana'); + expect(event.properties).to.have.property('kibana_version'); + expect(event.properties.kibana_version).to.be.a('string'); + + if (browser.isChromium) { + expect(event.properties).to.have.property('memory_js_heap_size_limit'); + expect(event.properties.memory_js_heap_size_limit).to.be.a('number'); + expect(event.properties).to.have.property('memory_js_heap_size_total'); + expect(event.properties.memory_js_heap_size_total).to.be.a('number'); + expect(event.properties).to.have.property('memory_js_heap_size_used'); + expect(event.properties.memory_js_heap_size_used).to.be.a('number'); + } + }); + }); +} diff --git a/test/analytics/tests/instrumented_events/from_the_server/core_context_providers.ts b/test/analytics/tests/instrumented_events/from_the_server/core_context_providers.ts new file mode 100644 index 0000000000000..743a32fcc58ac --- /dev/null +++ b/test/analytics/tests/instrumented_events/from_the_server/core_context_providers.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { Event } from '@kbn/core/public'; +import { FtrProviderContext } from '../../../services'; + +export default function ({ getService }: FtrProviderContext) { + const deployment = getService('deployment'); + const ebtServerHelper = getService('kibana_ebt_server'); + + describe('Core Context Providers', () => { + let event: Event; + before(async () => { + // Wait for the 2nd "status_changed" event. At that point all the context providers should be set up. + [, event] = await ebtServerHelper.getLastEvents(2, ['core-overall_status_changed']); + }); + + it('should have the properties provided by the "kibana info" context provider', () => { + expect(event.context).to.have.property('kibana_uuid'); + expect(event.context.kibana_uuid).to.be.a('string'); + expect(event.context).to.have.property('pid'); + expect(event.context.pid).to.be.a('number'); + }); + + it('should have the properties provided by the "build info" context provider', () => { + expect(event.context).to.have.property('isDev'); + expect(event.context.isDev).to.be.a('boolean'); + expect(event.context).to.have.property('isDistributable'); + expect(event.context.isDistributable).to.be.a('boolean'); + expect(event.context).to.have.property('version'); + expect(event.context.version).to.be.a('string'); + expect(event.context).to.have.property('branch'); + expect(event.context.branch).to.be.a('string'); + expect(event.context).to.have.property('buildNum'); + expect(event.context.buildNum).to.be.a('number'); + expect(event.context).to.have.property('buildSha'); + expect(event.context.buildSha).to.be.a('string'); + }); + + it('should have the properties provided by the "cluster info" context provider', () => { + expect(event.context).to.have.property('cluster_uuid'); + expect(event.context.cluster_uuid).to.be.a('string'); + expect(event.context).to.have.property('cluster_name'); + expect(event.context.cluster_name).to.be.a('string'); + expect(event.context).to.have.property('cluster_version'); + expect(event.context.cluster_version).to.be.a('string'); + }); + + it('should have the properties provided by the "status info" context provider', () => { + expect(event.context).to.have.property('overall_status_level'); + expect(event.context.overall_status_level).to.be.a('string'); + expect(event.context).to.have.property('overall_status_summary'); + expect(event.context.overall_status_summary).to.be.a('string'); + }); + + it('should have the properties provided by the "license info" context provider', () => { + expect(event.context).to.have.property('license_id'); + expect(event.context.license_id).to.be.a('string'); + expect(event.context).to.have.property('license_status'); + expect(event.context.license_status).to.be.a('string'); + expect(event.context).to.have.property('license_type'); + expect(event.context.license_type).to.be.a('string'); + }); + + it('should have the properties provided by the "Cloud Deployment ID" context provider', async () => { + if (await deployment.isCloud()) { + expect(event.context).to.have.property('cloudId'); + expect(event.context.cloudId).to.be.a('string'); + } else { + expect(event.context).not.to.have.property('cloudId'); + } + }); + }); +} diff --git a/test/analytics/tests/instrumented_events/from_the_server/core_overall_status_changed.ts b/test/analytics/tests/instrumented_events/from_the_server/core_overall_status_changed.ts new file mode 100644 index 0000000000000..fa94e2b69fc3f --- /dev/null +++ b/test/analytics/tests/instrumented_events/from_the_server/core_overall_status_changed.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { Event } from '@kbn/analytics-client'; +import { FtrProviderContext } from '../../../services'; + +export default function ({ getService }: FtrProviderContext) { + const ebtServerHelper = getService('kibana_ebt_server'); + + describe('core-overall_status_changed', () => { + let initialEvent: Event; + let secondEvent: Event; + + before(async () => { + [initialEvent, secondEvent] = await ebtServerHelper.getLastEvents(2, [ + 'core-overall_status_changed', + ]); + }); + + it('should emit the initial "degraded" event with the context set to `initializing`', () => { + expect(initialEvent.event_type).to.eql('core-overall_status_changed'); + expect(initialEvent.context).to.have.property('overall_status_level', 'initializing'); + expect(initialEvent.context).to.have.property( + 'overall_status_summary', + 'Kibana is starting up' + ); + expect(initialEvent.properties).to.have.property('overall_status_level', 'degraded'); + expect(initialEvent.properties.overall_status_summary).to.be.a('string'); + }); + + it('should emit the 2nd event as `available` with the context set to the previous values', () => { + expect(secondEvent.event_type).to.eql('core-overall_status_changed'); + expect(secondEvent.context).to.have.property( + 'overall_status_level', + initialEvent.properties.overall_status_level + ); + expect(secondEvent.context).to.have.property( + 'overall_status_summary', + initialEvent.properties.overall_status_summary + ); + expect(secondEvent.properties.overall_status_level).to.be.a('string'); // Ideally we would test it as `available`, but we can't do that as it may result flaky for many side effects in the CI. + expect(secondEvent.properties.overall_status_summary).to.be.a('string'); + }); + }); +} diff --git a/test/analytics/tests/instrumented_events/from_the_server/index.ts b/test/analytics/tests/instrumented_events/from_the_server/index.ts index 8961b9e92994c..d8150b0519fde 100644 --- a/test/analytics/tests/instrumented_events/from_the_server/index.ts +++ b/test/analytics/tests/instrumented_events/from_the_server/index.ts @@ -8,13 +8,11 @@ import { FtrProviderContext } from '../../../services'; -export default function ({ getService }: FtrProviderContext) { +export default function ({ loadTestFile }: FtrProviderContext) { describe('from the server', () => { - beforeEach(async () => { - await getService('kibana_ebt_server').setOptIn(true); - }); - - // Add tests for UI-instrumented events here: - // loadTestFile(require.resolve('./some_event')); + // Add tests for Server-instrumented events here: + loadTestFile(require.resolve('./core_context_providers')); + loadTestFile(require.resolve('./kibana_started')); + loadTestFile(require.resolve('./core_overall_status_changed')); }); } diff --git a/test/analytics/tests/instrumented_events/from_the_server/kibana_started.ts b/test/analytics/tests/instrumented_events/from_the_server/kibana_started.ts new file mode 100644 index 0000000000000..86917b937cbab --- /dev/null +++ b/test/analytics/tests/instrumented_events/from_the_server/kibana_started.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../services'; + +export default function ({ getService }: FtrProviderContext) { + const ebtServerHelper = getService('kibana_ebt_server'); + + describe('kibana_started', () => { + it('should emit the "kibana_started" event', async () => { + const [event] = await ebtServerHelper.getLastEvents(1, ['kibana_started']); + expect(event.event_type).to.eql('kibana_started'); + expect(event.properties.uptime_per_step.constructor.start).to.be.a('number'); + expect(event.properties.uptime_per_step.constructor.end).to.be.a('number'); + expect(event.properties.uptime_per_step.preboot.start).to.be.a('number'); + expect(event.properties.uptime_per_step.preboot.end).to.be.a('number'); + expect(event.properties.uptime_per_step.setup.start).to.be.a('number'); + expect(event.properties.uptime_per_step.setup.end).to.be.a('number'); + expect(event.properties.uptime_per_step.start.start).to.be.a('number'); + expect(event.properties.uptime_per_step.start.end).to.be.a('number'); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects/lib/saved_objects_test_utils.ts b/test/api_integration/apis/saved_objects/lib/saved_objects_test_utils.ts index d915fc75ed0c3..4e6fc6158e881 100644 --- a/test/api_integration/apis/saved_objects/lib/saved_objects_test_utils.ts +++ b/test/api_integration/apis/saved_objects/lib/saved_objects_test_utils.ts @@ -14,5 +14,6 @@ export async function getKibanaVersion(getService: FtrProviderContext['getServic const kibanaVersion = await kibanaServer.version.get(); expect(typeof kibanaVersion).to.eql('string'); expect(kibanaVersion.length).to.be.greaterThan(0); - return kibanaVersion; + // mimic SavedObjectsService.stripVersionQualifier() + return kibanaVersion.split('-')[0]; } diff --git a/test/api_integration/config.js b/test/api_integration/config.js index 4988094dad7a2..7f3f4b45298d1 100644 --- a/test/api_integration/config.js +++ b/test/api_integration/config.js @@ -10,7 +10,7 @@ import { services } from './services'; export default async function ({ readConfigFile }) { const commonConfig = await readConfigFile(require.resolve('../common/config')); - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { rootTags: ['runOutsideOfCiGroups'], diff --git a/test/examples/bfetch_explorer/index.ts b/test/examples/bfetch_explorer/index.ts index 247cef07a487e..b487704663c62 100644 --- a/test/examples/bfetch_explorer/index.ts +++ b/test/examples/bfetch_explorer/index.ts @@ -14,7 +14,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid const PageObjects = getPageObjects(['common', 'header']); describe('bfetch explorer', function () { - this.tags('ciGroup11'); before(async () => { await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('bfetch-explorer', { insertTimestamp: false }); diff --git a/test/examples/config.js b/test/examples/config.js index 6d1f1ec472350..25537a22e19ac 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -12,7 +12,7 @@ import fs from 'fs'; import { KIBANA_ROOT } from '@kbn/test'; export default async function ({ readConfigFile }) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); // Find all folders in /examples and /x-pack/examples since we treat all them as plugin folder const examplesFiles = fs.readdirSync(resolve(KIBANA_ROOT, 'examples')); diff --git a/test/examples/data_view_field_editor_example/index.ts b/test/examples/data_view_field_editor_example/index.ts index 0f8517cb3ed29..fcb8300749f0c 100644 --- a/test/examples/data_view_field_editor_example/index.ts +++ b/test/examples/data_view_field_editor_example/index.ts @@ -20,7 +20,6 @@ export default function ({ const PageObjects = getPageObjects(['common', 'header', 'settings']); describe('data view field editor example', function () { - this.tags('ciGroup11'); before(async () => { await esArchiver.emptyKibanaIndex(); await browser.setWindowSize(1300, 900); diff --git a/test/examples/embeddables/index.ts b/test/examples/embeddables/index.ts index 364c4001383a7..6cd95c699e7b8 100644 --- a/test/examples/embeddables/index.ts +++ b/test/examples/embeddables/index.ts @@ -18,7 +18,6 @@ export default function ({ const PageObjects = getPageObjects(['common', 'header']); describe('embeddable explorer', function () { - this.tags('ciGroup11'); before(async () => { await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('embeddableExplorer'); diff --git a/test/examples/expressions_explorer/index.ts b/test/examples/expressions_explorer/index.ts index 34f3c77cb0d3e..d7a47b63bd012 100644 --- a/test/examples/expressions_explorer/index.ts +++ b/test/examples/expressions_explorer/index.ts @@ -18,7 +18,6 @@ export default function ({ const PageObjects = getPageObjects(['common', 'header']); describe('expressions explorer', function () { - this.tags('ciGroup11'); before(async () => { await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('expressionsExplorer'); diff --git a/test/examples/field_formats/index.ts b/test/examples/field_formats/index.ts index f9692c910fda0..aebd92728b1af 100644 --- a/test/examples/field_formats/index.ts +++ b/test/examples/field_formats/index.ts @@ -16,7 +16,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Field formats example', function () { before(async () => { - this.tags('ciGroup11'); await PageObjects.common.navigateToApp('fieldFormatsExample'); }); diff --git a/test/examples/hello_world/index.ts b/test/examples/hello_world/index.ts index 1ffb7ff6d69af..604d014401b8f 100644 --- a/test/examples/hello_world/index.ts +++ b/test/examples/hello_world/index.ts @@ -17,7 +17,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid describe('Hello world', function () { before(async () => { - this.tags('ciGroup11'); await PageObjects.common.navigateToApp('helloWorld'); }); diff --git a/test/examples/partial_results/index.ts b/test/examples/partial_results/index.ts index 84ccff4cd35b7..6dc76f6a8856c 100644 --- a/test/examples/partial_results/index.ts +++ b/test/examples/partial_results/index.ts @@ -16,7 +16,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Partial Results Example', function () { before(async () => { - this.tags('ciGroup11'); await PageObjects.common.navigateToApp('partialResultsExample'); const element = await testSubjects.find('example-help'); diff --git a/test/examples/routing/index.ts b/test/examples/routing/index.ts index 949d8cfc7547a..0012283d8535f 100644 --- a/test/examples/routing/index.ts +++ b/test/examples/routing/index.ts @@ -17,7 +17,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid describe('routing examples', function () { before(async () => { - this.tags('ciGroup11'); await PageObjects.common.navigateToApp('routingExample'); }); diff --git a/test/examples/state_sync/index.ts b/test/examples/state_sync/index.ts index a33c014f4dd9d..6f70794497bef 100644 --- a/test/examples/state_sync/index.ts +++ b/test/examples/state_sync/index.ts @@ -17,7 +17,6 @@ export default function ({ const browser = getService('browser'); describe('state sync examples', function () { - this.tags('ciGroup11'); before(async () => { await browser.setWindowSize(1300, 900); }); diff --git a/test/examples/ui_actions/index.ts b/test/examples/ui_actions/index.ts index b04d361cc86ec..400af962053ba 100644 --- a/test/examples/ui_actions/index.ts +++ b/test/examples/ui_actions/index.ts @@ -18,7 +18,6 @@ export default function ({ const PageObjects = getPageObjects(['common', 'header']); describe('ui actions explorer', function () { - this.tags('ciGroup11'); before(async () => { await browser.setWindowSize(1300, 900); await PageObjects.common.navigateToApp('uiActionsExplorer'); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/constants.ts b/test/functional/apps/bundles/config.ts similarity index 53% rename from src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/constants.ts rename to test/functional/apps/bundles/config.ts index 1301c4b57d380..e487d31dcb657 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_counters/rollups/constants.ts +++ b/test/functional/apps/bundles/config.ts @@ -6,17 +6,13 @@ * Side Public License, v 1. */ -/** - * Roll indices every 24h - */ -export const ROLL_INDICES_INTERVAL = 24 * 60 * 60 * 1000; +import { FtrConfigProviderContext } from '@kbn/test'; -/** - * Start rolling indices after 5 minutes up - */ -export const ROLL_INDICES_START = 5 * 60 * 1000; +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); -/** - * Number of days to keep the UI counters saved object documents - */ -export const UI_COUNTERS_KEEP_DOCS_FOR_DAYS = 3; + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/bundles/index.js b/test/functional/apps/bundles/index.js index c3ce4201a470c..105aa155a9f1f 100644 --- a/test/functional/apps/bundles/index.js +++ b/test/functional/apps/bundles/index.js @@ -14,7 +14,7 @@ export default function ({ getService }) { const supertest = getService('supertest'); describe('bundle compression', function () { - this.tags(['ciGroup11', 'skipCoverage']); + this.tags('skipCoverage'); let buildNum; before(async () => { diff --git a/test/functional/apps/console/config.ts b/test/functional/apps/console/config.ts new file mode 100644 index 0000000000000..e487d31dcb657 --- /dev/null +++ b/test/functional/apps/console/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/console/index.js b/test/functional/apps/console/index.js index c3d0553514cb5..1944e10b5239f 100644 --- a/test/functional/apps/console/index.js +++ b/test/functional/apps/console/index.js @@ -10,8 +10,6 @@ export default function ({ getService, loadTestFile }) { const browser = getService('browser'); describe('console app', function () { - this.tags('ciGroup1'); - before(async function () { await browser.setWindowSize(1300, 1100); }); diff --git a/test/functional/apps/context/config.ts b/test/functional/apps/context/config.ts new file mode 100644 index 0000000000000..e487d31dcb657 --- /dev/null +++ b/test/functional/apps/context/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/context/index.ts b/test/functional/apps/context/index.ts index 1320a22aad09b..20e1bcc2a3cb4 100644 --- a/test/functional/apps/context/index.ts +++ b/test/functional/apps/context/index.ts @@ -15,8 +15,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid const kibanaServer = getService('kibanaServer'); describe('context app', function () { - this.tags('ciGroup1'); - before(async () => { await browser.setWindowSize(1200, 800); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); diff --git a/test/functional/apps/dashboard/README.md b/test/functional/apps/dashboard/README.md new file mode 100644 index 0000000000000..5e87a8b210bdd --- /dev/null +++ b/test/functional/apps/dashboard/README.md @@ -0,0 +1,7 @@ +# What are all these groups? + +These tests take a while so they have been broken up into groups with their own `config.ts` and `index.ts` file, causing each of these groups to be independent bundles of tests which can be run on some worker in CI without taking an incredible amount of time. + +Want to change the groups to something more logical? Have fun! Just make sure that each group executes on CI in less than 10 minutes or so. We don't currently have any mechanism for validating this right now, you just need to look at the times in the log output on CI, but we'll be working on tooling for making this information more accessible soon. + +- Kibana Operations \ No newline at end of file diff --git a/test/functional/apps/dashboard/group1/config.ts b/test/functional/apps/dashboard/group1/config.ts new file mode 100644 index 0000000000000..a70a190ca63f8 --- /dev/null +++ b/test/functional/apps/dashboard/group1/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.ts b/test/functional/apps/dashboard/group1/create_and_add_embeddables.ts similarity index 99% rename from test/functional/apps/dashboard/create_and_add_embeddables.ts rename to test/functional/apps/dashboard/group1/create_and_add_embeddables.ts index 30100f0e1aa07..c96e596a88ecf 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.ts +++ b/test/functional/apps/dashboard/group1/create_and_add_embeddables.ts @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import { VisualizeConstants } from '@kbn/visualizations-plugin/common/constants'; import { VISUALIZE_ENABLE_LABS_SETTING } from '@kbn/visualizations-plugin/common/constants'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/test/functional/apps/dashboard/dashboard_back_button.ts b/test/functional/apps/dashboard/group1/dashboard_back_button.ts similarity index 96% rename from test/functional/apps/dashboard/dashboard_back_button.ts rename to test/functional/apps/dashboard/group1/dashboard_back_button.ts index d532444befdab..1fd9614d2421a 100644 --- a/test/functional/apps/dashboard/dashboard_back_button.ts +++ b/test/functional/apps/dashboard/group1/dashboard_back_button.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); diff --git a/test/functional/apps/dashboard/dashboard_error_handling.ts b/test/functional/apps/dashboard/group1/dashboard_error_handling.ts similarity index 97% rename from test/functional/apps/dashboard/dashboard_error_handling.ts rename to test/functional/apps/dashboard/group1/dashboard_error_handling.ts index 58304359458c7..e950b8aef975d 100644 --- a/test/functional/apps/dashboard/dashboard_error_handling.ts +++ b/test/functional/apps/dashboard/group1/dashboard_error_handling.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'common']); diff --git a/test/functional/apps/dashboard/dashboard_options.ts b/test/functional/apps/dashboard/group1/dashboard_options.ts similarity index 96% rename from test/functional/apps/dashboard/dashboard_options.ts rename to test/functional/apps/dashboard/group1/dashboard_options.ts index 282674d0cec98..096f8595072bf 100644 --- a/test/functional/apps/dashboard/dashboard_options.ts +++ b/test/functional/apps/dashboard/group1/dashboard_options.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/test/functional/apps/dashboard/dashboard_query_bar.ts b/test/functional/apps/dashboard/group1/dashboard_query_bar.ts similarity index 96% rename from test/functional/apps/dashboard/dashboard_query_bar.ts rename to test/functional/apps/dashboard/group1/dashboard_query_bar.ts index 5092cadaf9d26..290cc62dca58f 100644 --- a/test/functional/apps/dashboard/dashboard_query_bar.ts +++ b/test/functional/apps/dashboard/group1/dashboard_query_bar.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/test/functional/apps/dashboard/dashboard_unsaved_listing.ts b/test/functional/apps/dashboard/group1/dashboard_unsaved_listing.ts similarity index 99% rename from test/functional/apps/dashboard/dashboard_unsaved_listing.ts rename to test/functional/apps/dashboard/group1/dashboard_unsaved_listing.ts index a1db57784b5f8..6b55a44ff9e79 100644 --- a/test/functional/apps/dashboard/dashboard_unsaved_listing.ts +++ b/test/functional/apps/dashboard/group1/dashboard_unsaved_listing.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); diff --git a/test/functional/apps/dashboard/dashboard_unsaved_state.ts b/test/functional/apps/dashboard/group1/dashboard_unsaved_state.ts similarity index 99% rename from test/functional/apps/dashboard/dashboard_unsaved_state.ts rename to test/functional/apps/dashboard/group1/dashboard_unsaved_state.ts index 5afe3b9937433..2447a122a77aa 100644 --- a/test/functional/apps/dashboard/dashboard_unsaved_state.ts +++ b/test/functional/apps/dashboard/group1/dashboard_unsaved_state.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); diff --git a/test/functional/apps/dashboard/data_shared_attributes.ts b/test/functional/apps/dashboard/group1/data_shared_attributes.ts similarity index 98% rename from test/functional/apps/dashboard/data_shared_attributes.ts rename to test/functional/apps/dashboard/group1/data_shared_attributes.ts index a94cf1b6063a9..d4070c700a925 100644 --- a/test/functional/apps/dashboard/data_shared_attributes.ts +++ b/test/functional/apps/dashboard/group1/data_shared_attributes.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/test/functional/apps/dashboard/edit_embeddable_redirects.ts b/test/functional/apps/dashboard/group1/edit_embeddable_redirects.ts similarity index 98% rename from test/functional/apps/dashboard/edit_embeddable_redirects.ts rename to test/functional/apps/dashboard/group1/edit_embeddable_redirects.ts index 763488cc21ab1..aca22d84e6843 100644 --- a/test/functional/apps/dashboard/edit_embeddable_redirects.ts +++ b/test/functional/apps/dashboard/group1/edit_embeddable_redirects.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); diff --git a/test/functional/apps/dashboard/edit_visualizations.js b/test/functional/apps/dashboard/group1/edit_visualizations.js similarity index 100% rename from test/functional/apps/dashboard/edit_visualizations.js rename to test/functional/apps/dashboard/group1/edit_visualizations.js diff --git a/test/functional/apps/dashboard/embed_mode.ts b/test/functional/apps/dashboard/group1/embed_mode.ts similarity index 94% rename from test/functional/apps/dashboard/embed_mode.ts rename to test/functional/apps/dashboard/group1/embed_mode.ts index 7e53bff7387ca..482c976d98689 100644 --- a/test/functional/apps/dashboard/embed_mode.ts +++ b/test/functional/apps/dashboard/group1/embed_mode.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); @@ -59,7 +59,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.missingOrFail('top-nav'); await testSubjects.missingOrFail('queryInput'); await testSubjects.missingOrFail('superDatePickerToggleQuickMenuButton'); - await testSubjects.existOrFail('showFilterActions'); + await testSubjects.existOrFail('showQueryBarMenu'); const currentUrl = await browser.getCurrentUrl(); const newUrl = [currentUrl].concat(urlParamExtensions).join('&'); @@ -70,7 +70,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('top-nav'); await testSubjects.existOrFail('queryInput'); await testSubjects.existOrFail('superDatePickerToggleQuickMenuButton'); - await testSubjects.missingOrFail('showFilterActions'); }); after(async function () { diff --git a/test/functional/apps/dashboard/embeddable_data_grid.ts b/test/functional/apps/dashboard/group1/embeddable_data_grid.ts similarity index 97% rename from test/functional/apps/dashboard/embeddable_data_grid.ts rename to test/functional/apps/dashboard/group1/embeddable_data_grid.ts index 060c467656662..85277e63d6f6c 100644 --- a/test/functional/apps/dashboard/embeddable_data_grid.ts +++ b/test/functional/apps/dashboard/group1/embeddable_data_grid.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardAddPanel = getService('dashboardAddPanel'); diff --git a/test/functional/apps/dashboard/embeddable_rendering.ts b/test/functional/apps/dashboard/group1/embeddable_rendering.ts similarity index 99% rename from test/functional/apps/dashboard/embeddable_rendering.ts rename to test/functional/apps/dashboard/group1/embeddable_rendering.ts index 840826be46532..5274a2c12e878 100644 --- a/test/functional/apps/dashboard/embeddable_rendering.ts +++ b/test/functional/apps/dashboard/group1/embeddable_rendering.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; /** * This tests both that one of each visualization can be added to a dashboard (as opposed to opening an existing diff --git a/test/functional/apps/dashboard/empty_dashboard.ts b/test/functional/apps/dashboard/group1/empty_dashboard.ts similarity index 97% rename from test/functional/apps/dashboard/empty_dashboard.ts rename to test/functional/apps/dashboard/group1/empty_dashboard.ts index a7524eaa94b8a..e559c0ef81f60 100644 --- a/test/functional/apps/dashboard/empty_dashboard.ts +++ b/test/functional/apps/dashboard/group1/empty_dashboard.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/test/functional/apps/dashboard/group1/index.ts b/test/functional/apps/dashboard/group1/index.ts new file mode 100644 index 0000000000000..597102433ef45 --- /dev/null +++ b/test/functional/apps/dashboard/group1/index.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + + async function loadCurrentData() { + await browser.setWindowSize(1300, 900); + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/dashboard/current/data'); + } + + async function unloadCurrentData() { + await esArchiver.unload('test/functional/fixtures/es_archiver/dashboard/current/data'); + } + + describe('dashboard app - group 1', function () { + before(loadCurrentData); + after(unloadCurrentData); + + // This has to be first since the other tests create some embeddables as side affects and our counting assumes + // a fresh index. + loadTestFile(require.resolve('./empty_dashboard')); + loadTestFile(require.resolve('./url_field_formatter')); + loadTestFile(require.resolve('./embeddable_rendering')); + loadTestFile(require.resolve('./embeddable_data_grid')); + loadTestFile(require.resolve('./create_and_add_embeddables')); + loadTestFile(require.resolve('./edit_embeddable_redirects')); + loadTestFile(require.resolve('./dashboard_unsaved_state')); + loadTestFile(require.resolve('./dashboard_unsaved_listing')); + loadTestFile(require.resolve('./edit_visualizations')); + loadTestFile(require.resolve('./dashboard_options')); + loadTestFile(require.resolve('./data_shared_attributes')); + loadTestFile(require.resolve('./share')); + loadTestFile(require.resolve('./embed_mode')); + loadTestFile(require.resolve('./dashboard_back_button')); + loadTestFile(require.resolve('./dashboard_error_handling')); + loadTestFile(require.resolve('./legacy_urls')); + loadTestFile(require.resolve('./saved_search_embeddable')); + + // Note: This one must be last because it unloads some data for one of its tests! + // No, this isn't ideal, but loading/unloading takes so much time and these are all bunched + // to improve efficiency... + loadTestFile(require.resolve('./dashboard_query_bar')); + }); +} diff --git a/test/functional/apps/dashboard/legacy_urls.ts b/test/functional/apps/dashboard/group1/legacy_urls.ts similarity index 98% rename from test/functional/apps/dashboard/legacy_urls.ts rename to test/functional/apps/dashboard/group1/legacy_urls.ts index 1e4138e63d393..e11da2d82fe47 100644 --- a/test/functional/apps/dashboard/legacy_urls.ts +++ b/test/functional/apps/dashboard/group1/legacy_urls.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/test/functional/apps/dashboard/saved_search_embeddable.ts b/test/functional/apps/dashboard/group1/saved_search_embeddable.ts similarity index 98% rename from test/functional/apps/dashboard/saved_search_embeddable.ts rename to test/functional/apps/dashboard/group1/saved_search_embeddable.ts index 02050eec30227..e0ecc40d2486b 100644 --- a/test/functional/apps/dashboard/saved_search_embeddable.ts +++ b/test/functional/apps/dashboard/group1/saved_search_embeddable.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardAddPanel = getService('dashboardAddPanel'); diff --git a/test/functional/apps/dashboard/share.ts b/test/functional/apps/dashboard/group1/share.ts similarity index 95% rename from test/functional/apps/dashboard/share.ts rename to test/functional/apps/dashboard/group1/share.ts index 7fe8048ab7c04..871ab5bed1488 100644 --- a/test/functional/apps/dashboard/share.ts +++ b/test/functional/apps/dashboard/group1/share.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); diff --git a/test/functional/apps/dashboard/url_field_formatter.ts b/test/functional/apps/dashboard/group1/url_field_formatter.ts similarity index 95% rename from test/functional/apps/dashboard/url_field_formatter.ts rename to test/functional/apps/dashboard/group1/url_field_formatter.ts index 8e9dd7b66e79f..be454549af378 100644 --- a/test/functional/apps/dashboard/url_field_formatter.ts +++ b/test/functional/apps/dashboard/group1/url_field_formatter.ts @@ -7,8 +7,8 @@ */ import expect from '@kbn/expect'; -import { WebElementWrapper } from '../../services/lib/web_element_wrapper'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { WebElementWrapper } from '../../../services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const { common, dashboard, settings, timePicker, visChart } = getPageObjects([ diff --git a/test/functional/apps/dashboard/group2/config.ts b/test/functional/apps/dashboard/group2/config.ts new file mode 100644 index 0000000000000..a70a190ca63f8 --- /dev/null +++ b/test/functional/apps/dashboard/group2/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.ts b/test/functional/apps/dashboard/group2/dashboard_filter_bar.ts similarity index 99% rename from test/functional/apps/dashboard/dashboard_filter_bar.ts rename to test/functional/apps/dashboard/group2/dashboard_filter_bar.ts index 3f74c4bc2f0dc..966b453409433 100644 --- a/test/functional/apps/dashboard/dashboard_filter_bar.ts +++ b/test/functional/apps/dashboard/group2/dashboard_filter_bar.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dataGrid = getService('dataGrid'); diff --git a/test/functional/apps/dashboard/dashboard_filtering.ts b/test/functional/apps/dashboard/group2/dashboard_filtering.ts similarity index 99% rename from test/functional/apps/dashboard/dashboard_filtering.ts rename to test/functional/apps/dashboard/group2/dashboard_filtering.ts index 9522c47f907fc..09acbd5965020 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.ts +++ b/test/functional/apps/dashboard/group2/dashboard_filtering.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; /** * Test the querying capabilities of dashboard, and make sure visualizations show the expected results, especially diff --git a/test/functional/apps/dashboard/dashboard_grid.ts b/test/functional/apps/dashboard/group2/dashboard_grid.ts similarity index 96% rename from test/functional/apps/dashboard/dashboard_grid.ts rename to test/functional/apps/dashboard/group2/dashboard_grid.ts index 25e901fd25d8b..90e2187e19eb4 100644 --- a/test/functional/apps/dashboard/dashboard_grid.ts +++ b/test/functional/apps/dashboard/group2/dashboard_grid.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); diff --git a/test/functional/apps/dashboard/dashboard_saved_query.ts b/test/functional/apps/dashboard/group2/dashboard_saved_query.ts similarity index 82% rename from test/functional/apps/dashboard/dashboard_saved_query.ts rename to test/functional/apps/dashboard/group2/dashboard_saved_query.ts index 658afb9c641b2..1dad54234e8a3 100644 --- a/test/functional/apps/dashboard/dashboard_saved_query.ts +++ b/test/functional/apps/dashboard/group2/dashboard_saved_query.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); @@ -40,22 +40,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickNewDashboard(); }); - it('should show the saved query management component when there are no saved queries', async () => { - await savedQueryManagementComponent.openSavedQueryManagementComponent(); - const descriptionText = await testSubjects.getVisibleText('saved-query-management-popover'); - expect(descriptionText).to.eql( - 'Saved Queries\nThere are no saved queries. Save query text and filters that you want to use again.\nSave current query' - ); + it('should show the saved query management load button as disabled when there are no saved queries', async () => { + await testSubjects.click('showQueryBarMenu'); + const loadFilterSetBtn = await testSubjects.find('saved-query-management-load-button'); + const isDisabled = await loadFilterSetBtn.getAttribute('disabled'); + expect(isDisabled).to.equal('true'); }); it('should allow a query to be saved via the saved objects management component', async () => { await queryBar.setQuery('response:200'); + await queryBar.clickQuerySubmitButton(); + await testSubjects.click('showQueryBarMenu'); await savedQueryManagementComponent.saveNewQuery( 'OkResponse', '200 responses for .jpg over 24 hours', true, true ); + const contextMenuPanelTitleButton = await testSubjects.exists( + 'contextMenuPanelTitleButton' + ); + if (contextMenuPanelTitleButton) { + await testSubjects.click('contextMenuPanelTitleButton'); + } + await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); await savedQueryManagementComponent.savedQueryTextExist('response:200'); }); @@ -81,6 +89,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await queryBar.setQuery('response:404'); await savedQueryManagementComponent.updateCurrentlyLoadedQuery('OkResponse', false, false); await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + const contextMenuPanelTitleButton = await testSubjects.exists( + 'contextMenuPanelTitleButton' + ); + if (contextMenuPanelTitleButton) { + await testSubjects.click('contextMenuPanelTitleButton'); + } await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); expect(await queryBar.getQueryString()).to.eql(''); await savedQueryManagementComponent.loadSavedQuery('OkResponse'); @@ -88,9 +102,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('allows saving the currently loaded query as a new query', async () => { + await queryBar.setQuery('response:400'); await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( 'OkResponseCopy', - '200 responses', + '400 responses', false, false ); diff --git a/test/functional/apps/dashboard/dashboard_snapshots.ts b/test/functional/apps/dashboard/group2/dashboard_snapshots.ts similarity index 98% rename from test/functional/apps/dashboard/dashboard_snapshots.ts rename to test/functional/apps/dashboard/group2/dashboard_snapshots.ts index 9cb52c5dd5511..dc1a74ea74b7d 100644 --- a/test/functional/apps/dashboard/dashboard_snapshots.ts +++ b/test/functional/apps/dashboard/group2/dashboard_snapshots.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, diff --git a/test/functional/apps/dashboard/embeddable_library.ts b/test/functional/apps/dashboard/group2/embeddable_library.ts similarity index 97% rename from test/functional/apps/dashboard/embeddable_library.ts rename to test/functional/apps/dashboard/group2/embeddable_library.ts index 2abf75f6385ac..ca52eaecaf46e 100644 --- a/test/functional/apps/dashboard/embeddable_library.ts +++ b/test/functional/apps/dashboard/group2/embeddable_library.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); diff --git a/test/functional/apps/dashboard/full_screen_mode.ts b/test/functional/apps/dashboard/group2/full_screen_mode.ts similarity index 98% rename from test/functional/apps/dashboard/full_screen_mode.ts rename to test/functional/apps/dashboard/group2/full_screen_mode.ts index 74fa2168a1461..35d9ed8a2a15c 100644 --- a/test/functional/apps/dashboard/full_screen_mode.ts +++ b/test/functional/apps/dashboard/group2/full_screen_mode.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/test/functional/apps/dashboard/group2/index.ts b/test/functional/apps/dashboard/group2/index.ts new file mode 100644 index 0000000000000..004c85f2da760 --- /dev/null +++ b/test/functional/apps/dashboard/group2/index.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + + async function loadCurrentData() { + await browser.setWindowSize(1300, 900); + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/dashboard/current/data'); + } + + async function unloadCurrentData() { + await esArchiver.unload('test/functional/fixtures/es_archiver/dashboard/current/data'); + } + + describe('dashboard app - group 2', function () { + before(loadCurrentData); + after(unloadCurrentData); + + loadTestFile(require.resolve('./full_screen_mode')); + loadTestFile(require.resolve('./dashboard_filter_bar')); + loadTestFile(require.resolve('./dashboard_filtering')); + loadTestFile(require.resolve('./panel_expand_toggle')); + loadTestFile(require.resolve('./dashboard_grid')); + loadTestFile(require.resolve('./view_edit')); + loadTestFile(require.resolve('./dashboard_saved_query')); + // Order of test suites *shouldn't* be important but there's a bug for the view_edit test above + // https://github.com/elastic/kibana/issues/46752 + // The dashboard_snapshot test below requires the timestamped URL which breaks the view_edit test. + // If we don't use the timestamp in the URL, the colors in the charts will be different. + loadTestFile(require.resolve('./dashboard_snapshots')); + loadTestFile(require.resolve('./embeddable_library')); + }); +} diff --git a/test/functional/apps/dashboard/panel_expand_toggle.ts b/test/functional/apps/dashboard/group2/panel_expand_toggle.ts similarity index 97% rename from test/functional/apps/dashboard/panel_expand_toggle.ts rename to test/functional/apps/dashboard/group2/panel_expand_toggle.ts index 272ec3824e233..f33280ba7bb79 100644 --- a/test/functional/apps/dashboard/panel_expand_toggle.ts +++ b/test/functional/apps/dashboard/group2/panel_expand_toggle.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/test/functional/apps/dashboard/view_edit.ts b/test/functional/apps/dashboard/group2/view_edit.ts similarity index 99% rename from test/functional/apps/dashboard/view_edit.ts rename to test/functional/apps/dashboard/group2/view_edit.ts index a73924a8ae75f..dfd62eeaa6cb3 100644 --- a/test/functional/apps/dashboard/view_edit.ts +++ b/test/functional/apps/dashboard/group2/view_edit.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const queryBar = getService('queryBar'); diff --git a/test/functional/apps/dashboard/bwc_shared_urls.ts b/test/functional/apps/dashboard/group3/bwc_shared_urls.ts similarity index 99% rename from test/functional/apps/dashboard/bwc_shared_urls.ts rename to test/functional/apps/dashboard/group3/bwc_shared_urls.ts index 569cd8e2a67d5..01b1c8379089e 100644 --- a/test/functional/apps/dashboard/bwc_shared_urls.ts +++ b/test/functional/apps/dashboard/group3/bwc_shared_urls.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header']); diff --git a/test/functional/apps/dashboard/group3/config.ts b/test/functional/apps/dashboard/group3/config.ts new file mode 100644 index 0000000000000..a70a190ca63f8 --- /dev/null +++ b/test/functional/apps/dashboard/group3/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/dashboard/copy_panel_to.ts b/test/functional/apps/dashboard/group3/copy_panel_to.ts similarity index 98% rename from test/functional/apps/dashboard/copy_panel_to.ts rename to test/functional/apps/dashboard/group3/copy_panel_to.ts index 9a61b289ee1f3..1f40f780a5398 100644 --- a/test/functional/apps/dashboard/copy_panel_to.ts +++ b/test/functional/apps/dashboard/group3/copy_panel_to.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardVisualizations = getService('dashboardVisualizations'); diff --git a/test/functional/apps/dashboard/dashboard_state.ts b/test/functional/apps/dashboard/group3/dashboard_state.ts similarity index 97% rename from test/functional/apps/dashboard/dashboard_state.ts rename to test/functional/apps/dashboard/group3/dashboard_state.ts index d931475766776..c5306f4ab4ff3 100644 --- a/test/functional/apps/dashboard/dashboard_state.ts +++ b/test/functional/apps/dashboard/group3/dashboard_state.ts @@ -10,8 +10,8 @@ import expect from '@kbn/expect'; import chroma from 'chroma-js'; import { DEFAULT_PANEL_WIDTH } from '@kbn/dashboard-plugin/public/application/embeddable/dashboard_constants'; -import { PIE_CHART_VIS_NAME, AREA_CHART_VIS_NAME } from '../../page_objects/dashboard_page'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { PIE_CHART_VIS_NAME, AREA_CHART_VIS_NAME } from '../../../page_objects/dashboard_page'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ @@ -48,13 +48,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { isNewChartsLibraryEnabled = await PageObjects.visChart.isNewChartsLibraryEnabled(); await PageObjects.dashboard.initTests(); await PageObjects.dashboard.preserveCrossAppState(); + await browser.setLocalStorageItem('data.newDataViewMenu', 'true'); if (isNewChartsLibraryEnabled) { await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyPieChartsLibrary': false, }); - await browser.refresh(); } + await browser.refresh(); }); after(async function () { diff --git a/test/functional/apps/dashboard/dashboard_time_picker.ts b/test/functional/apps/dashboard/group3/dashboard_time_picker.ts similarity index 97% rename from test/functional/apps/dashboard/dashboard_time_picker.ts rename to test/functional/apps/dashboard/group3/dashboard_time_picker.ts index 6f876185fd8dd..37f6e4f2ef5df 100644 --- a/test/functional/apps/dashboard/dashboard_time_picker.ts +++ b/test/functional/apps/dashboard/group3/dashboard_time_picker.ts @@ -8,8 +8,8 @@ import expect from '@kbn/expect'; -import { PIE_CHART_VIS_NAME } from '../../page_objects/dashboard_page'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { PIE_CHART_VIS_NAME } from '../../../page_objects/dashboard_page'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardExpect = getService('dashboardExpect'); diff --git a/test/functional/apps/dashboard/group3/index.ts b/test/functional/apps/dashboard/group3/index.ts new file mode 100644 index 0000000000000..f3a10500fe4e6 --- /dev/null +++ b/test/functional/apps/dashboard/group3/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + + async function loadLogstash() { + await browser.setWindowSize(1200, 900); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + } + + async function unloadLogstash() { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + } + + describe('dashboard app - group 3', function () { + before(loadLogstash); + after(unloadLogstash); + + loadTestFile(require.resolve('./dashboard_time_picker')); + loadTestFile(require.resolve('./bwc_shared_urls')); + loadTestFile(require.resolve('./panel_replacing')); + loadTestFile(require.resolve('./panel_cloning')); + loadTestFile(require.resolve('./copy_panel_to')); + loadTestFile(require.resolve('./panel_context_menu')); + loadTestFile(require.resolve('./dashboard_state')); + }); +} diff --git a/test/functional/apps/dashboard/panel_cloning.ts b/test/functional/apps/dashboard/group3/panel_cloning.ts similarity index 96% rename from test/functional/apps/dashboard/panel_cloning.ts rename to test/functional/apps/dashboard/group3/panel_cloning.ts index a2cadd89f486a..4de65419d2ecb 100644 --- a/test/functional/apps/dashboard/panel_cloning.ts +++ b/test/functional/apps/dashboard/group3/panel_cloning.ts @@ -7,8 +7,8 @@ */ import expect from '@kbn/expect'; -import { PIE_CHART_VIS_NAME } from '../../page_objects/dashboard_page'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { PIE_CHART_VIS_NAME } from '../../../page_objects/dashboard_page'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); diff --git a/test/functional/apps/dashboard/panel_context_menu.ts b/test/functional/apps/dashboard/group3/panel_context_menu.ts similarity index 98% rename from test/functional/apps/dashboard/panel_context_menu.ts rename to test/functional/apps/dashboard/group3/panel_context_menu.ts index 8c82c162f5e86..f78cd27614b3b 100644 --- a/test/functional/apps/dashboard/panel_context_menu.ts +++ b/test/functional/apps/dashboard/group3/panel_context_menu.ts @@ -8,8 +8,8 @@ import expect from '@kbn/expect'; import { VisualizeConstants } from '@kbn/visualizations-plugin/common/constants'; -import { PIE_CHART_VIS_NAME } from '../../page_objects/dashboard_page'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { PIE_CHART_VIS_NAME } from '../../../page_objects/dashboard_page'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); diff --git a/test/functional/apps/dashboard/panel_replacing.ts b/test/functional/apps/dashboard/group3/panel_replacing.ts similarity index 97% rename from test/functional/apps/dashboard/panel_replacing.ts rename to test/functional/apps/dashboard/group3/panel_replacing.ts index b9ba731beee29..e6ff8c4f940bb 100644 --- a/test/functional/apps/dashboard/panel_replacing.ts +++ b/test/functional/apps/dashboard/group3/panel_replacing.ts @@ -11,8 +11,8 @@ import { PIE_CHART_VIS_NAME, AREA_CHART_VIS_NAME, LINE_CHART_VIS_NAME, -} from '../../page_objects/dashboard_page'; -import { FtrProviderContext } from '../../ftr_provider_context'; +} from '../../../page_objects/dashboard_page'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); diff --git a/test/functional/apps/dashboard/group4/config.ts b/test/functional/apps/dashboard/group4/config.ts new file mode 100644 index 0000000000000..a70a190ca63f8 --- /dev/null +++ b/test/functional/apps/dashboard/group4/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/dashboard/dashboard_clone.ts b/test/functional/apps/dashboard/group4/dashboard_clone.ts similarity index 97% rename from test/functional/apps/dashboard/dashboard_clone.ts rename to test/functional/apps/dashboard/group4/dashboard_clone.ts index 5fc0b0c28c914..26738760b28e4 100644 --- a/test/functional/apps/dashboard/dashboard_clone.ts +++ b/test/functional/apps/dashboard/group4/dashboard_clone.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/test/functional/apps/dashboard/dashboard_listing.ts b/test/functional/apps/dashboard/group4/dashboard_listing.ts similarity index 99% rename from test/functional/apps/dashboard/dashboard_listing.ts rename to test/functional/apps/dashboard/group4/dashboard_listing.ts index 9182a0d318228..4a9827ce02f91 100644 --- a/test/functional/apps/dashboard/dashboard_listing.ts +++ b/test/functional/apps/dashboard/group4/dashboard_listing.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'common']); diff --git a/test/functional/apps/dashboard/dashboard_save.ts b/test/functional/apps/dashboard/group4/dashboard_save.ts similarity index 98% rename from test/functional/apps/dashboard/dashboard_save.ts rename to test/functional/apps/dashboard/group4/dashboard_save.ts index 4ab8633a5619b..f20817c65d25d 100644 --- a/test/functional/apps/dashboard/dashboard_save.ts +++ b/test/functional/apps/dashboard/group4/dashboard_save.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'visualize']); diff --git a/test/functional/apps/dashboard/dashboard_time.ts b/test/functional/apps/dashboard/group4/dashboard_time.ts similarity index 98% rename from test/functional/apps/dashboard/dashboard_time.ts rename to test/functional/apps/dashboard/group4/dashboard_time.ts index 2c0394474adab..2ff91185be60a 100644 --- a/test/functional/apps/dashboard/dashboard_time.ts +++ b/test/functional/apps/dashboard/group4/dashboard_time.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; const dashboardName = 'Dashboard Test Time'; diff --git a/test/functional/apps/dashboard/group4/index.ts b/test/functional/apps/dashboard/group4/index.ts new file mode 100644 index 0000000000000..8c9a291d2e577 --- /dev/null +++ b/test/functional/apps/dashboard/group4/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + + async function loadLogstash() { + await browser.setWindowSize(1200, 900); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + } + + async function unloadLogstash() { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + } + + describe('dashboard app - group 4', function () { + before(loadLogstash); + after(unloadLogstash); + + loadTestFile(require.resolve('./dashboard_save')); + loadTestFile(require.resolve('./dashboard_time')); + loadTestFile(require.resolve('./dashboard_listing')); + loadTestFile(require.resolve('./dashboard_clone')); + }); +} diff --git a/test/functional/apps/dashboard/group5/config.ts b/test/functional/apps/dashboard/group5/config.ts new file mode 100644 index 0000000000000..a70a190ca63f8 --- /dev/null +++ b/test/functional/apps/dashboard/group5/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/dashboard/group5/index.ts b/test/functional/apps/dashboard/group5/index.ts new file mode 100644 index 0000000000000..14f4a6366477d --- /dev/null +++ b/test/functional/apps/dashboard/group5/index.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 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + + async function loadLogstash() { + await browser.setWindowSize(1200, 900); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + } + + async function unloadLogstash() { + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + } + + describe('dashboard app - group 5', function () { + // TODO: Remove when vislib is removed + // https://github.com/elastic/kibana/issues/56143 + describe('new charts library', function () { + before(async () => { + await loadLogstash(); + await kibanaServer.uiSettings.update({ + 'visualization:visualize:legacyPieChartsLibrary': false, + }); + await browser.refresh(); + }); + + after(async () => { + await unloadLogstash(); + await kibanaServer.uiSettings.update({ + 'visualization:visualize:legacyPieChartsLibrary': true, + }); + await browser.refresh(); + }); + + loadTestFile(require.resolve('../group3/dashboard_state')); + }); + }); +} diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts deleted file mode 100644 index 4f69504ef7d5c..0000000000000 --- a/test/functional/apps/dashboard/index.ts +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, loadTestFile }: FtrProviderContext) { - const browser = getService('browser'); - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - - async function loadCurrentData() { - await browser.setWindowSize(1300, 900); - await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/dashboard/current/data'); - } - - async function unloadCurrentData() { - await esArchiver.unload('test/functional/fixtures/es_archiver/dashboard/current/data'); - } - - async function loadLogstash() { - await browser.setWindowSize(1200, 900); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - } - - async function unloadLogstash() { - await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); - } - - describe('dashboard app', function () { - // This has to be first since the other tests create some embeddables as side affects and our counting assumes - // a fresh index. - describe('using current data', function () { - this.tags('ciGroup2'); - before(loadCurrentData); - after(unloadCurrentData); - - loadTestFile(require.resolve('./empty_dashboard')); - loadTestFile(require.resolve('./url_field_formatter')); - loadTestFile(require.resolve('./embeddable_rendering')); - loadTestFile(require.resolve('./embeddable_data_grid')); - loadTestFile(require.resolve('./create_and_add_embeddables')); - loadTestFile(require.resolve('./edit_embeddable_redirects')); - loadTestFile(require.resolve('./dashboard_unsaved_state')); - loadTestFile(require.resolve('./dashboard_unsaved_listing')); - loadTestFile(require.resolve('./edit_visualizations')); - loadTestFile(require.resolve('./dashboard_options')); - loadTestFile(require.resolve('./data_shared_attributes')); - loadTestFile(require.resolve('./share')); - loadTestFile(require.resolve('./embed_mode')); - loadTestFile(require.resolve('./dashboard_back_button')); - loadTestFile(require.resolve('./dashboard_error_handling')); - loadTestFile(require.resolve('./legacy_urls')); - loadTestFile(require.resolve('./saved_search_embeddable')); - - // Note: This one must be last because it unloads some data for one of its tests! - // No, this isn't ideal, but loading/unloading takes so much time and these are all bunched - // to improve efficiency... - loadTestFile(require.resolve('./dashboard_query_bar')); - }); - - describe('using current data', function () { - this.tags('ciGroup3'); - before(loadCurrentData); - after(unloadCurrentData); - - loadTestFile(require.resolve('./full_screen_mode')); - loadTestFile(require.resolve('./dashboard_filter_bar')); - loadTestFile(require.resolve('./dashboard_filtering')); - loadTestFile(require.resolve('./panel_expand_toggle')); - loadTestFile(require.resolve('./dashboard_grid')); - loadTestFile(require.resolve('./view_edit')); - loadTestFile(require.resolve('./dashboard_saved_query')); - // Order of test suites *shouldn't* be important but there's a bug for the view_edit test above - // https://github.com/elastic/kibana/issues/46752 - // The dashboard_snapshot test below requires the timestamped URL which breaks the view_edit test. - // If we don't use the timestamp in the URL, the colors in the charts will be different. - loadTestFile(require.resolve('./dashboard_snapshots')); - loadTestFile(require.resolve('./embeddable_library')); - }); - - // Each of these tests call initTests themselves, the way it was originally written. The above tests only load - // the data once to save on time. Eventually, all of these tests should just use current data and we can reserve - // legacy data only for specifically testing BWC situations. - describe('using legacy data', function () { - this.tags('ciGroup4'); - before(loadLogstash); - after(unloadLogstash); - - loadTestFile(require.resolve('./dashboard_time_picker')); - loadTestFile(require.resolve('./bwc_shared_urls')); - loadTestFile(require.resolve('./panel_replacing')); - loadTestFile(require.resolve('./panel_cloning')); - loadTestFile(require.resolve('./copy_panel_to')); - loadTestFile(require.resolve('./panel_context_menu')); - loadTestFile(require.resolve('./dashboard_state')); - }); - - describe('using legacy data', function () { - this.tags('ciGroup5'); - before(loadLogstash); - after(unloadLogstash); - - loadTestFile(require.resolve('./dashboard_save')); - loadTestFile(require.resolve('./dashboard_time')); - loadTestFile(require.resolve('./dashboard_listing')); - loadTestFile(require.resolve('./dashboard_clone')); - }); - - // TODO: Remove when vislib is removed - // https://github.com/elastic/kibana/issues/56143 - describe('new charts library', function () { - this.tags('ciGroup5'); - - before(async () => { - await loadLogstash(); - await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyPieChartsLibrary': false, - }); - await browser.refresh(); - }); - - after(async () => { - await unloadLogstash(); - await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyPieChartsLibrary': true, - }); - await browser.refresh(); - }); - - loadTestFile(require.resolve('./dashboard_state')); - }); - }); -} diff --git a/test/functional/apps/dashboard_elements/config.ts b/test/functional/apps/dashboard_elements/config.ts new file mode 100644 index 0000000000000..e487d31dcb657 --- /dev/null +++ b/test/functional/apps/dashboard_elements/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/dashboard_elements/controls/options_list.ts b/test/functional/apps/dashboard_elements/controls/options_list.ts index a4da1c217f92b..de3144d98beab 100644 --- a/test/functional/apps/dashboard_elements/controls/options_list.ts +++ b/test/functional/apps/dashboard_elements/controls/options_list.ts @@ -286,7 +286,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.waitForRenderComplete(); await header.waitUntilLoadingHasFinished(); await ensureAvailableOptionsEql(allAvailableOptions); - await filterBar.removeAllFilters(); + await filterBar.removeFilter('sound.keyword'); }); it('Does not apply time range to options list control', async () => { @@ -406,6 +406,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Options List dashboard no validation', async () => { before(async () => { + await filterBar.removeAllFilters(); + await queryBar.clickQuerySubmitButton(); await dashboardControls.optionsListOpenPopover(controlId); await dashboardControls.optionsListPopoverSelectOption('meow'); await dashboardControls.optionsListPopoverSelectOption('bark'); @@ -431,6 +433,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await filterBar.removeAllFilters(); + await queryBar.clickQuerySubmitButton(); await dashboardControls.clearAllControls(); }); }); diff --git a/test/functional/apps/dashboard_elements/index.ts b/test/functional/apps/dashboard_elements/index.ts index 6bd3e4e04a9c9..10b0c6e5ecff5 100644 --- a/test/functional/apps/dashboard_elements/index.ts +++ b/test/functional/apps/dashboard_elements/index.ts @@ -29,9 +29,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('test/functional/fixtures/es_archiver/long_window_logstash'); }); - describe('dashboard elements ciGroup10', function () { - this.tags('ciGroup10'); - + describe('dashboard elements', function () { loadTestFile(require.resolve('./input_control_vis')); loadTestFile(require.resolve('./controls')); loadTestFile(require.resolve('./_markdown_vis')); diff --git a/test/functional/apps/discover/_indexpattern_without_timefield.ts b/test/functional/apps/discover/_indexpattern_without_timefield.ts index 2d5892fa6e6ca..6c936f63e999d 100644 --- a/test/functional/apps/discover/_indexpattern_without_timefield.ts +++ b/test/functional/apps/discover/_indexpattern_without_timefield.ts @@ -91,7 +91,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.goBack(); await PageObjects.discover.waitForDocTableLoadingComplete(); return ( - (await testSubjects.getVisibleText('indexPattern-switch-link')) === 'without-timefield' + (await testSubjects.getVisibleText('discover-dataView-switch-link')) === + 'without-timefield' ); } ); diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index 79d49131df138..d56b5032a430b 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -144,12 +144,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('saved query management component functionality', function () { before(async () => await setUpQueriesWithFilters()); - it('should show the saved query management component when there are no saved queries', async () => { + it('should show the saved query management load button as disabled when there are no saved queries', async () => { await savedQueryManagementComponent.openSavedQueryManagementComponent(); - const descriptionText = await testSubjects.getVisibleText('saved-query-management-popover'); - expect(descriptionText).to.eql( - 'Saved Queries\nThere are no saved queries. Save query text and filters that you want to use again.\nSave current query' - ); + const loadFilterSetBtn = await testSubjects.find('saved-query-management-load-button'); + const isDisabled = await loadFilterSetBtn.getAttribute('disabled'); + expect(isDisabled).to.equal('true'); }); it('should allow a query to be saved via the saved objects management component', async () => { @@ -189,9 +188,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('allows saving changes to a currently loaded query via the saved query management component', async () => { + await savedQueryManagementComponent.closeSavedQueryManagementComponent(); await queryBar.setQuery('response:404'); await savedQueryManagementComponent.updateCurrentlyLoadedQuery('OkResponse', false, false); await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + const contextMenuPanelTitleButton = await testSubjects.exists( + 'contextMenuPanelTitleButton' + ); + if (contextMenuPanelTitleButton) { + await testSubjects.click('contextMenuPanelTitleButton'); + } await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); expect(await queryBar.getQueryString()).to.eql(''); await savedQueryManagementComponent.loadSavedQuery('OkResponse'); @@ -199,9 +205,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('allows saving the currently loaded query as a new query', async () => { + await queryBar.setQuery('response:400'); await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( 'OkResponseCopy', - '200 responses', + '400 responses', false, false ); @@ -215,6 +222,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('does not allow saving a query with a non-unique name', async () => { + // this check allows this test to run stand alone, also should fix occacional flakiness + const savedQueryExists = await savedQueryManagementComponent.savedQueryExist('OkResponse'); + if (!savedQueryExists) { + await savedQueryManagementComponent.saveNewQuery( + 'OkResponse', + '200 responses for .jpg over 24 hours', + true, + true + ); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + } + await queryBar.setQuery('response:400'); await savedQueryManagementComponent.saveNewQueryWithNameError('OkResponse'); }); @@ -232,17 +251,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('allows clearing if non default language was remembered in localstorage', async () => { + await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.switchQueryLanguage('lucene'); await PageObjects.common.navigateToApp('discover'); // makes sure discovered is reloaded without any state in url + await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.expectQueryLanguageOrFail('lucene'); // make sure lucene is remembered after refresh (comes from localstorage) await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.expectQueryLanguageOrFail('kql'); await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.expectQueryLanguageOrFail('lucene'); }); it('changing language removes saved query', async () => { await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.switchQueryLanguage('lucene'); expect(await queryBar.getQueryString()).to.eql(''); }); diff --git a/test/functional/apps/discover/config.ts b/test/functional/apps/discover/config.ts new file mode 100644 index 0000000000000..e487d31dcb657 --- /dev/null +++ b/test/functional/apps/discover/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index e2895f3ca56b4..20f8f017b084f 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -13,8 +13,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const config = getService('config'); describe('discover app', function () { - this.tags('ciGroup6'); - before(async function () { await browser.setWindowSize(1300, 800); }); diff --git a/test/functional/apps/getting_started/config.ts b/test/functional/apps/getting_started/config.ts new file mode 100644 index 0000000000000..e487d31dcb657 --- /dev/null +++ b/test/functional/apps/getting_started/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/getting_started/index.ts b/test/functional/apps/getting_started/index.ts index c88999e23be3d..a0506ce52e028 100644 --- a/test/functional/apps/getting_started/index.ts +++ b/test/functional/apps/getting_started/index.ts @@ -13,8 +13,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); describe('Getting Started ', function () { - this.tags(['ciGroup5']); - before(async function () { await browser.setWindowSize(1200, 800); }); diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts index 016cead53f0c4..1d9d02d5e94b5 100644 --- a/test/functional/apps/home/_navigation.ts +++ b/test/functional/apps/home/_navigation.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); - const PageObjects = getPageObjects(['common', 'header', 'home', 'timePicker']); + const PageObjects = getPageObjects(['common', 'header', 'home', 'timePicker', 'unifiedSearch']); const appsMenu = getService('appsMenu'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); @@ -37,6 +37,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Navigate to discover app await appsMenu.clickLink('Discover'); + await PageObjects.unifiedSearch.closeTourPopoverByLocalStorage(); const discoverUrl = await browser.getCurrentUrl(); await PageObjects.timePicker.setDefaultAbsoluteRange(); const modifiedTimeDiscoverUrl = await browser.getCurrentUrl(); diff --git a/test/functional/apps/home/config.ts b/test/functional/apps/home/config.ts new file mode 100644 index 0000000000000..e487d31dcb657 --- /dev/null +++ b/test/functional/apps/home/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/home/index.js b/test/functional/apps/home/index.js index 992c5b1f2f474..ada4aa54073cf 100644 --- a/test/functional/apps/home/index.js +++ b/test/functional/apps/home/index.js @@ -9,9 +9,7 @@ export default function ({ getService, loadTestFile }) { const browser = getService('browser'); - describe('homepage app', function () { - this.tags('ciGroup5'); - + describe('home app', function () { before(function () { return browser.setWindowSize(1200, 800); }); diff --git a/test/functional/apps/management/config.ts b/test/functional/apps/management/config.ts new file mode 100644 index 0000000000000..e487d31dcb657 --- /dev/null +++ b/test/functional/apps/management/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index a4271ae73d9e2..840d04d0d1aed 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -21,33 +21,24 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('test/functional/fixtures/es_archiver/makelogs'); }); - describe('', function () { - this.tags('ciGroup9'); - - loadTestFile(require.resolve('./_create_index_pattern_wizard')); - loadTestFile(require.resolve('./_index_pattern_create_delete')); - loadTestFile(require.resolve('./_index_pattern_results_sort')); - loadTestFile(require.resolve('./_index_pattern_popularity')); - loadTestFile(require.resolve('./_kibana_settings')); - loadTestFile(require.resolve('./_scripted_fields_preview')); - loadTestFile(require.resolve('./_mgmt_import_saved_objects')); - loadTestFile(require.resolve('./_index_patterns_empty')); - loadTestFile(require.resolve('./_scripted_fields')); - loadTestFile(require.resolve('./_runtime_fields')); - loadTestFile(require.resolve('./_field_formatter')); - loadTestFile(require.resolve('./_legacy_url_redirect')); - loadTestFile(require.resolve('./_exclude_index_pattern')); - }); - - describe('', function () { - this.tags('ciGroup8'); - - loadTestFile(require.resolve('./_index_pattern_filter')); - loadTestFile(require.resolve('./_scripted_fields_filter')); - loadTestFile(require.resolve('./_import_objects')); - loadTestFile(require.resolve('./_test_huge_fields')); - loadTestFile(require.resolve('./_handle_alias')); - loadTestFile(require.resolve('./_handle_version_conflict')); - }); + loadTestFile(require.resolve('./_create_index_pattern_wizard')); + loadTestFile(require.resolve('./_index_pattern_create_delete')); + loadTestFile(require.resolve('./_index_pattern_results_sort')); + loadTestFile(require.resolve('./_index_pattern_popularity')); + loadTestFile(require.resolve('./_kibana_settings')); + loadTestFile(require.resolve('./_scripted_fields_preview')); + loadTestFile(require.resolve('./_mgmt_import_saved_objects')); + loadTestFile(require.resolve('./_index_patterns_empty')); + loadTestFile(require.resolve('./_scripted_fields')); + loadTestFile(require.resolve('./_runtime_fields')); + loadTestFile(require.resolve('./_field_formatter')); + loadTestFile(require.resolve('./_legacy_url_redirect')); + loadTestFile(require.resolve('./_exclude_index_pattern')); + loadTestFile(require.resolve('./_index_pattern_filter')); + loadTestFile(require.resolve('./_scripted_fields_filter')); + loadTestFile(require.resolve('./_import_objects')); + loadTestFile(require.resolve('./_test_huge_fields')); + loadTestFile(require.resolve('./_handle_alias')); + loadTestFile(require.resolve('./_handle_version_conflict')); }); } diff --git a/test/functional/apps/saved_objects_management/config.ts b/test/functional/apps/saved_objects_management/config.ts new file mode 100644 index 0000000000000..e487d31dcb657 --- /dev/null +++ b/test/functional/apps/saved_objects_management/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/saved_objects_management/index.ts b/test/functional/apps/saved_objects_management/index.ts index 12e0cc8863f12..c70b2c6d25b17 100644 --- a/test/functional/apps/saved_objects_management/index.ts +++ b/test/functional/apps/saved_objects_management/index.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function savedObjectsManagementApp({ loadTestFile }: FtrProviderContext) { describe('saved objects management', function savedObjectsManagementAppTestSuite() { - this.tags('ciGroup7'); loadTestFile(require.resolve('./inspect_saved_objects')); loadTestFile(require.resolve('./show_relationships')); }); diff --git a/test/functional/apps/status_page/config.ts b/test/functional/apps/status_page/config.ts new file mode 100644 index 0000000000000..e487d31dcb657 --- /dev/null +++ b/test/functional/apps/status_page/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/status_page/index.ts b/test/functional/apps/status_page/index.ts index 509abeb4f0346..971f9c4984c99 100644 --- a/test/functional/apps/status_page/index.ts +++ b/test/functional/apps/status_page/index.ts @@ -14,8 +14,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common']); describe('status page', function () { - this.tags('ciGroup1'); - beforeEach(async () => { await PageObjects.common.navigateToApp('status_page'); }); diff --git a/test/functional/apps/visualize/README.md b/test/functional/apps/visualize/README.md new file mode 100644 index 0000000000000..5e87a8b210bdd --- /dev/null +++ b/test/functional/apps/visualize/README.md @@ -0,0 +1,7 @@ +# What are all these groups? + +These tests take a while so they have been broken up into groups with their own `config.ts` and `index.ts` file, causing each of these groups to be independent bundles of tests which can be run on some worker in CI without taking an incredible amount of time. + +Want to change the groups to something more logical? Have fun! Just make sure that each group executes on CI in less than 10 minutes or so. We don't currently have any mechanism for validating this right now, you just need to look at the times in the log output on CI, but we'll be working on tooling for making this information more accessible soon. + +- Kibana Operations \ No newline at end of file diff --git a/test/functional/apps/visualize/_chart_types.ts b/test/functional/apps/visualize/group1/_chart_types.ts similarity index 96% rename from test/functional/apps/visualize/_chart_types.ts rename to test/functional/apps/visualize/group1/_chart_types.ts index 1afc372f75b0e..4b5922e21a51c 100644 --- a/test/functional/apps/visualize/_chart_types.ts +++ b/test/functional/apps/visualize/group1/_chart_types.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_data_table.ts b/test/functional/apps/visualize/group1/_data_table.ts similarity index 99% rename from test/functional/apps/visualize/_data_table.ts rename to test/functional/apps/visualize/group1/_data_table.ts index e165e40e83dc1..9b95c5b69fd41 100644 --- a/test/functional/apps/visualize/_data_table.ts +++ b/test/functional/apps/visualize/group1/_data_table.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_data_table_nontimeindex.ts b/test/functional/apps/visualize/group1/_data_table_nontimeindex.ts similarity index 98% rename from test/functional/apps/visualize/_data_table_nontimeindex.ts rename to test/functional/apps/visualize/group1/_data_table_nontimeindex.ts index 1549f2aac0735..43407d3a899ea 100644 --- a/test/functional/apps/visualize/_data_table_nontimeindex.ts +++ b/test/functional/apps/visualize/group1/_data_table_nontimeindex.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts b/test/functional/apps/visualize/group1/_data_table_notimeindex_filters.ts similarity index 97% rename from test/functional/apps/visualize/_data_table_notimeindex_filters.ts rename to test/functional/apps/visualize/group1/_data_table_notimeindex_filters.ts index 51ceef947bfac..d62bdd86ecf9b 100644 --- a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts +++ b/test/functional/apps/visualize/group1/_data_table_notimeindex_filters.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_embedding_chart.ts b/test/functional/apps/visualize/group1/_embedding_chart.ts similarity index 98% rename from test/functional/apps/visualize/_embedding_chart.ts rename to test/functional/apps/visualize/group1/_embedding_chart.ts index 9531eafc33bed..a07e3a36e2aea 100644 --- a/test/functional/apps/visualize/_embedding_chart.ts +++ b/test/functional/apps/visualize/group1/_embedding_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); diff --git a/test/functional/apps/visualize/group1/config.ts b/test/functional/apps/visualize/group1/config.ts new file mode 100644 index 0000000000000..a70a190ca63f8 --- /dev/null +++ b/test/functional/apps/visualize/group1/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/visualize/group1/index.ts b/test/functional/apps/visualize/group1/index.ts new file mode 100644 index 0000000000000..fa3379b632cc1 --- /dev/null +++ b/test/functional/apps/visualize/group1/index.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 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + describe('visualize app - group1', () => { + before(async () => { + log.debug('Starting visualize before method'); + await browser.setWindowSize(1280, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); + }); + + loadTestFile(require.resolve('./_embedding_chart')); + loadTestFile(require.resolve('./_data_table')); + loadTestFile(require.resolve('./_data_table_nontimeindex')); + loadTestFile(require.resolve('./_data_table_notimeindex_filters')); + loadTestFile(require.resolve('./_chart_types')); + }); +} diff --git a/test/functional/apps/visualize/_experimental_vis.ts b/test/functional/apps/visualize/group2/_experimental_vis.ts similarity index 97% rename from test/functional/apps/visualize/_experimental_vis.ts rename to test/functional/apps/visualize/group2/_experimental_vis.ts index 8e33285f909be..26460192a6b96 100644 --- a/test/functional/apps/visualize/_experimental_vis.ts +++ b/test/functional/apps/visualize/group2/_experimental_vis.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_gauge_chart.ts b/test/functional/apps/visualize/group2/_gauge_chart.ts similarity index 97% rename from test/functional/apps/visualize/_gauge_chart.ts rename to test/functional/apps/visualize/group2/_gauge_chart.ts index 6dd460d4ac32b..08425fcd78b5f 100644 --- a/test/functional/apps/visualize/_gauge_chart.ts +++ b/test/functional/apps/visualize/group2/_gauge_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); @@ -102,6 +102,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should show correct values for fields with fieldFormatters', async () => { + await filterBar.removeAllFilters(); const expectedTexts = ['2,904', 'win 8: Count', '0B', 'win 8: Min bytes']; await PageObjects.visEditor.selectAggregation('Terms'); @@ -117,8 +118,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(expectedTexts).to.eql(metricValue); }); }); - - afterEach(async () => await filterBar.removeAllFilters()); }); }); } diff --git a/test/functional/apps/visualize/_heatmap_chart.ts b/test/functional/apps/visualize/group2/_heatmap_chart.ts similarity index 98% rename from test/functional/apps/visualize/_heatmap_chart.ts rename to test/functional/apps/visualize/group2/_heatmap_chart.ts index 54cec19be97ff..1c82b66273251 100644 --- a/test/functional/apps/visualize/_heatmap_chart.ts +++ b/test/functional/apps/visualize/group2/_heatmap_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_histogram_request_start.ts b/test/functional/apps/visualize/group2/_histogram_request_start.ts similarity index 98% rename from test/functional/apps/visualize/_histogram_request_start.ts rename to test/functional/apps/visualize/group2/_histogram_request_start.ts index 28ebb25744d3f..a12474d9ebc2e 100644 --- a/test/functional/apps/visualize/_histogram_request_start.ts +++ b/test/functional/apps/visualize/group2/_histogram_request_start.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_inspector.ts b/test/functional/apps/visualize/group2/_inspector.ts similarity index 98% rename from test/functional/apps/visualize/_inspector.ts rename to test/functional/apps/visualize/group2/_inspector.ts index f83eae2fc00bc..7b306f7817f5c 100644 --- a/test/functional/apps/visualize/_inspector.ts +++ b/test/functional/apps/visualize/group2/_inspector.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_metric_chart.ts b/test/functional/apps/visualize/group2/_metric_chart.ts similarity index 99% rename from test/functional/apps/visualize/_metric_chart.ts rename to test/functional/apps/visualize/group2/_metric_chart.ts index 7853a3a845bfc..b797ccb630363 100644 --- a/test/functional/apps/visualize/_metric_chart.ts +++ b/test/functional/apps/visualize/group2/_metric_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/group2/config.ts b/test/functional/apps/visualize/group2/config.ts new file mode 100644 index 0000000000000..a70a190ca63f8 --- /dev/null +++ b/test/functional/apps/visualize/group2/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/visualize/group2/index.ts b/test/functional/apps/visualize/group2/index.ts new file mode 100644 index 0000000000000..ea5ad24e2f873 --- /dev/null +++ b/test/functional/apps/visualize/group2/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + describe('visualize app', () => { + before(async () => { + log.debug('Starting visualize before method'); + await browser.setWindowSize(1280, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); + }); + + loadTestFile(require.resolve('./_inspector')); + loadTestFile(require.resolve('./_experimental_vis')); + loadTestFile(require.resolve('./_gauge_chart')); + loadTestFile(require.resolve('./_heatmap_chart')); + loadTestFile(require.resolve('./_histogram_request_start')); + loadTestFile(require.resolve('./_metric_chart')); + }); +} diff --git a/test/functional/apps/visualize/_add_to_dashboard.ts b/test/functional/apps/visualize/group3/_add_to_dashboard.ts similarity index 99% rename from test/functional/apps/visualize/_add_to_dashboard.ts rename to test/functional/apps/visualize/group3/_add_to_dashboard.ts index 9e8984675eccd..32d329cd181da 100644 --- a/test/functional/apps/visualize/_add_to_dashboard.ts +++ b/test/functional/apps/visualize/group3/_add_to_dashboard.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardExpect = getService('dashboardExpect'); diff --git a/test/functional/apps/visualize/_lab_mode.ts b/test/functional/apps/visualize/group3/_lab_mode.ts similarity index 97% rename from test/functional/apps/visualize/_lab_mode.ts rename to test/functional/apps/visualize/group3/_lab_mode.ts index 2af593f2acc4a..ba1b8ae33e2ce 100644 --- a/test/functional/apps/visualize/_lab_mode.ts +++ b/test/functional/apps/visualize/group3/_lab_mode.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { VISUALIZE_ENABLE_LABS_SETTING } from '@kbn/visualizations-plugin/common/constants'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_linked_saved_searches.ts b/test/functional/apps/visualize/group3/_linked_saved_searches.ts similarity index 98% rename from test/functional/apps/visualize/_linked_saved_searches.ts rename to test/functional/apps/visualize/group3/_linked_saved_searches.ts index 6fa8acac8e781..e64a3f18bde95 100644 --- a/test/functional/apps/visualize/_linked_saved_searches.ts +++ b/test/functional/apps/visualize/group3/_linked_saved_searches.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); diff --git a/test/functional/apps/visualize/_pie_chart.ts b/test/functional/apps/visualize/group3/_pie_chart.ts similarity index 95% rename from test/functional/apps/visualize/_pie_chart.ts rename to test/functional/apps/visualize/group3/_pie_chart.ts index 48d49d3007b68..23b008c690cba 100644 --- a/test/functional/apps/visualize/_pie_chart.ts +++ b/test/functional/apps/visualize/group3/_pie_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); @@ -401,7 +401,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ['360,000', '47', 'US', '4'], ['360,000', '47', 'BD', '3'], ['360,000', '47', 'BR', '2'], - ]; + ].map((row) => + // the count of records is not shown for every split level in the new charting library + isNewChartsLibraryEnabled ? [row[0], ...row.slice(2)] : row + ); await inspector.open(); await inspector.setTablePageSize(50); @@ -447,6 +450,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should still showing pie chart when a subseries have zero data', async function () { + if (isNewChartsLibraryEnabled) { + // TODO: it seems that adding a filter agg which has no results to a pie chart breaks it and instead it shows "no data" + return; + } + await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickPieChart'); await PageObjects.visualize.clickPieChart(); @@ -518,7 +526,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ['osx', '1,322', 'US', '130'], ['osx', '1,322', 'ID', '56'], ['osx', '1,322', 'BR', '30'], - ]; + ].map((row) => + // the count of records is not shown for every split level in the new charting library + isNewChartsLibraryEnabled ? [row[0], ...row.slice(2)] : row + ); await inspector.open(); await inspector.setTablePageSize(50); await inspector.expectTableData(expectedTableData); @@ -532,7 +543,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ['win xp', '526', 'CN', '526'], ['ios', '478', 'CN', '478'], ['osx', '228', 'CN', '228'], - ]; + ].map((row) => + // the count of records is not shown for every split level in the new charting library + isNewChartsLibraryEnabled ? [row[0], ...row.slice(2)] : row + ); await PageObjects.visChart.filterLegend('CN'); await PageObjects.header.waitUntilLoadingHasFinished(); await inspector.open(); diff --git a/test/functional/apps/visualize/_shared_item.ts b/test/functional/apps/visualize/group3/_shared_item.ts similarity index 95% rename from test/functional/apps/visualize/_shared_item.ts rename to test/functional/apps/visualize/group3/_shared_item.ts index 3f9016ca2ff82..0a84ae1962c63 100644 --- a/test/functional/apps/visualize/_shared_item.ts +++ b/test/functional/apps/visualize/group3/_shared_item.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_visualize_listing.ts b/test/functional/apps/visualize/group3/_visualize_listing.ts similarity index 98% rename from test/functional/apps/visualize/_visualize_listing.ts rename to test/functional/apps/visualize/group3/_visualize_listing.ts index 30b19b52af258..ad370939f2260 100644 --- a/test/functional/apps/visualize/_visualize_listing.ts +++ b/test/functional/apps/visualize/group3/_visualize_listing.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visEditor']); diff --git a/test/functional/apps/visualize/group3/config.ts b/test/functional/apps/visualize/group3/config.ts new file mode 100644 index 0000000000000..a70a190ca63f8 --- /dev/null +++ b/test/functional/apps/visualize/group3/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/visualize/group3/index.ts b/test/functional/apps/visualize/group3/index.ts new file mode 100644 index 0000000000000..93eff60575cb3 --- /dev/null +++ b/test/functional/apps/visualize/group3/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + describe('visualize app', () => { + before(async () => { + log.debug('Starting visualize before method'); + await browser.setWindowSize(1280, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); + }); + + loadTestFile(require.resolve('./_pie_chart')); + loadTestFile(require.resolve('./_shared_item')); + loadTestFile(require.resolve('./_lab_mode')); + loadTestFile(require.resolve('./_linked_saved_searches')); + loadTestFile(require.resolve('./_visualize_listing')); + loadTestFile(require.resolve('./_add_to_dashboard.ts')); + }); +} diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/group4/_tsvb_chart.ts similarity index 99% rename from test/functional/apps/visualize/_tsvb_chart.ts rename to test/functional/apps/visualize/group4/_tsvb_chart.ts index d462a89108c09..013c0473a59b9 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/group4/_tsvb_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); diff --git a/test/functional/apps/visualize/group4/config.ts b/test/functional/apps/visualize/group4/config.ts new file mode 100644 index 0000000000000..a70a190ca63f8 --- /dev/null +++ b/test/functional/apps/visualize/group4/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/visualize/group4/index.ts b/test/functional/apps/visualize/group4/index.ts new file mode 100644 index 0000000000000..3476489706415 --- /dev/null +++ b/test/functional/apps/visualize/group4/index.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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + describe('visualize app', () => { + before(async () => { + log.debug('Starting visualize before method'); + await browser.setWindowSize(1280, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); + }); + + loadTestFile(require.resolve('./_tsvb_chart')); + }); +} diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/group5/_tsvb_time_series.ts similarity index 99% rename from test/functional/apps/visualize/_tsvb_time_series.ts rename to test/functional/apps/visualize/group5/_tsvb_time_series.ts index 3f6661aaecf00..409b2b3610f5c 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/group5/_tsvb_time_series.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const { visualize, visualBuilder, timeToVisualize, dashboard, common } = getPageObjects([ diff --git a/test/functional/apps/visualize/group5/config.ts b/test/functional/apps/visualize/group5/config.ts new file mode 100644 index 0000000000000..a70a190ca63f8 --- /dev/null +++ b/test/functional/apps/visualize/group5/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/visualize/group5/index.ts b/test/functional/apps/visualize/group5/index.ts new file mode 100644 index 0000000000000..eafa39962ff56 --- /dev/null +++ b/test/functional/apps/visualize/group5/index.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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + describe('visualize app', () => { + before(async () => { + log.debug('Starting visualize before method'); + await browser.setWindowSize(1280, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); + }); + + loadTestFile(require.resolve('./_tsvb_time_series')); + }); +} diff --git a/test/functional/apps/visualize/_tag_cloud.ts b/test/functional/apps/visualize/group6/_tag_cloud.ts similarity index 99% rename from test/functional/apps/visualize/_tag_cloud.ts rename to test/functional/apps/visualize/group6/_tag_cloud.ts index 9380f40e0d36c..93a7fb22a2ca8 100644 --- a/test/functional/apps/visualize/_tag_cloud.ts +++ b/test/functional/apps/visualize/group6/_tag_cloud.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/test/functional/apps/visualize/_tsvb_markdown.ts b/test/functional/apps/visualize/group6/_tsvb_markdown.ts similarity index 99% rename from test/functional/apps/visualize/_tsvb_markdown.ts rename to test/functional/apps/visualize/group6/_tsvb_markdown.ts index 98ed05d854f0c..80756b1eacbfb 100644 --- a/test/functional/apps/visualize/_tsvb_markdown.ts +++ b/test/functional/apps/visualize/group6/_tsvb_markdown.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const { visualBuilder, timePicker, visualize, visChart } = getPageObjects([ diff --git a/test/functional/apps/visualize/_tsvb_table.ts b/test/functional/apps/visualize/group6/_tsvb_table.ts similarity index 99% rename from test/functional/apps/visualize/_tsvb_table.ts rename to test/functional/apps/visualize/group6/_tsvb_table.ts index ed668e4bca8e5..e7e24885cb406 100644 --- a/test/functional/apps/visualize/_tsvb_table.ts +++ b/test/functional/apps/visualize/group6/_tsvb_table.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const { visualBuilder, visualize, visChart, settings } = getPageObjects([ diff --git a/test/functional/apps/visualize/_vega_chart.ts b/test/functional/apps/visualize/group6/_vega_chart.ts similarity index 97% rename from test/functional/apps/visualize/_vega_chart.ts rename to test/functional/apps/visualize/group6/_vega_chart.ts index 6640b37b4a28a..1d802065ad137 100644 --- a/test/functional/apps/visualize/_vega_chart.ts +++ b/test/functional/apps/visualize/group6/_vega_chart.ts @@ -9,7 +9,7 @@ import { unzip } from 'lodash'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; const getTestSpec = (expression: string) => ` { @@ -220,7 +220,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Vega extension functions', () => { beforeEach(async () => { - await filterBar.removeAllFilters(); + const filtersCount = await filterBar.getFilterCount(); + if (filtersCount > 0) { + await filterBar.removeAllFilters(); + } }); const fillSpecAndGo = async (newSpec: string) => { diff --git a/test/functional/apps/visualize/group6/config.ts b/test/functional/apps/visualize/group6/config.ts new file mode 100644 index 0000000000000..a70a190ca63f8 --- /dev/null +++ b/test/functional/apps/visualize/group6/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/visualize/group6/index.ts b/test/functional/apps/visualize/group6/index.ts new file mode 100644 index 0000000000000..05fe3b232d370 --- /dev/null +++ b/test/functional/apps/visualize/group6/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + + describe('visualize app', () => { + before(async () => { + log.debug('Starting visualize before method'); + await browser.setWindowSize(1280, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); + }); + + loadTestFile(require.resolve('./_tag_cloud')); + loadTestFile(require.resolve('./_tsvb_markdown')); + loadTestFile(require.resolve('./_tsvb_table')); + loadTestFile(require.resolve('./_vega_chart')); + }); +} diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts deleted file mode 100644 index d68fb4b253123..0000000000000 --- a/test/functional/apps/visualize/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, loadTestFile }: FtrProviderContext) { - const browser = getService('browser'); - const log = getService('log'); - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - - describe('visualize app', () => { - before(async () => { - log.debug('Starting visualize before method'); - await browser.setWindowSize(1280, 800); - await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); - - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); - }); - - // TODO: Remove when vislib is removed - describe('new charts library visualize ciGroup7', function () { - this.tags('ciGroup7'); - - before(async () => { - await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyPieChartsLibrary': false, - 'visualization:visualize:legacyHeatmapChartsLibrary': false, - }); - await browser.refresh(); - }); - - after(async () => { - await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyPieChartsLibrary': true, - 'visualization:visualize:legacyHeatmapChartsLibrary': true, - }); - await browser.refresh(); - }); - - // Test replaced vislib chart types - loadTestFile(require.resolve('./_area_chart')); - loadTestFile(require.resolve('./_line_chart_split_series')); - loadTestFile(require.resolve('./_line_chart_split_chart')); - loadTestFile(require.resolve('./_point_series_options')); - loadTestFile(require.resolve('./_vertical_bar_chart')); - loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex')); - loadTestFile(require.resolve('./_pie_chart')); - loadTestFile(require.resolve('./_timelion')); - loadTestFile(require.resolve('./_heatmap_chart')); - }); - - describe('visualize ciGroup9', function () { - this.tags('ciGroup9'); - - loadTestFile(require.resolve('./_embedding_chart')); - loadTestFile(require.resolve('./_data_table')); - loadTestFile(require.resolve('./_data_table_nontimeindex')); - loadTestFile(require.resolve('./_data_table_notimeindex_filters')); - loadTestFile(require.resolve('./_chart_types')); - }); - - describe('visualize ciGroup10', function () { - this.tags('ciGroup10'); - - loadTestFile(require.resolve('./_inspector')); - loadTestFile(require.resolve('./_experimental_vis')); - loadTestFile(require.resolve('./_gauge_chart')); - loadTestFile(require.resolve('./_heatmap_chart')); - loadTestFile(require.resolve('./_histogram_request_start')); - loadTestFile(require.resolve('./_metric_chart')); - }); - - describe('visualize ciGroup1', function () { - this.tags('ciGroup1'); - - loadTestFile(require.resolve('./_pie_chart')); - loadTestFile(require.resolve('./_shared_item')); - loadTestFile(require.resolve('./_lab_mode')); - loadTestFile(require.resolve('./_linked_saved_searches')); - loadTestFile(require.resolve('./_visualize_listing')); - loadTestFile(require.resolve('./_add_to_dashboard.ts')); - }); - - describe('visualize ciGroup8', function () { - this.tags('ciGroup8'); - - loadTestFile(require.resolve('./_tsvb_chart')); - }); - - describe('visualize ciGroup11', function () { - this.tags('ciGroup11'); - - loadTestFile(require.resolve('./_tsvb_time_series')); - }); - - describe('visualize ciGroup12', function () { - this.tags('ciGroup12'); - - loadTestFile(require.resolve('./_tag_cloud')); - loadTestFile(require.resolve('./_tsvb_markdown')); - loadTestFile(require.resolve('./_tsvb_table')); - loadTestFile(require.resolve('./_vega_chart')); - }); - }); -} diff --git a/test/functional/apps/visualize/_area_chart.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_area_chart.ts similarity index 99% rename from test/functional/apps/visualize/_area_chart.ts rename to test/functional/apps/visualize/replaced_vislib_chart_types/_area_chart.ts index 76bb1d2f58d05..5fbb264910dca 100644 --- a/test/functional/apps/visualize/_area_chart.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_area_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_line_chart_split_chart.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_chart.ts similarity index 99% rename from test/functional/apps/visualize/_line_chart_split_chart.ts rename to test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_chart.ts index 0e44c30499ed3..77ddc3bbac1a4 100644 --- a/test/functional/apps/visualize/_line_chart_split_chart.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_line_chart_split_series.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_series.ts similarity index 99% rename from test/functional/apps/visualize/_line_chart_split_series.ts rename to test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_series.ts index d10b4ebd9b312..a46c46fda48ad 100644 --- a/test/functional/apps/visualize/_line_chart_split_series.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_line_chart_split_series.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_point_series_options.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_point_series_options.ts similarity index 99% rename from test/functional/apps/visualize/_point_series_options.ts rename to test/functional/apps/visualize/replaced_vislib_chart_types/_point_series_options.ts index a2d2831c87933..1a11d19064ce7 100644 --- a/test/functional/apps/visualize/_point_series_options.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_point_series_options.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_timelion.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_timelion.ts similarity index 99% rename from test/functional/apps/visualize/_timelion.ts rename to test/functional/apps/visualize/replaced_vislib_chart_types/_timelion.ts index e10ba03a0e19f..dc80083a67697 100644 --- a/test/functional/apps/visualize/_timelion.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_timelion.ts @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const { timePicker, visChart, visEditor, visualize, timelion, common } = getPageObjects([ diff --git a/test/functional/apps/visualize/_vertical_bar_chart.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart.ts similarity index 99% rename from test/functional/apps/visualize/_vertical_bar_chart.ts rename to test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart.ts index 7c4f989724ad9..b8d5cd64bbc1f 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart_nontimeindex.ts similarity index 99% rename from test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts rename to test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart_nontimeindex.ts index eadc7c58af5a5..4f00bac7792c4 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/_vertical_bar_chart_nontimeindex.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); diff --git a/test/functional/apps/visualize/replaced_vislib_chart_types/config.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/config.ts new file mode 100644 index 0000000000000..a70a190ca63f8 --- /dev/null +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/visualize/replaced_vislib_chart_types/index.ts b/test/functional/apps/visualize/replaced_vislib_chart_types/index.ts new file mode 100644 index 0000000000000..5794edef68555 --- /dev/null +++ b/test/functional/apps/visualize/replaced_vislib_chart_types/index.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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + + // TODO: Remove when vislib is removed + describe('visualize app - new charts library visualize', () => { + before(async () => { + log.debug('Starting visualize before method'); + await browser.setWindowSize(1280, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/empty_kibana'); + + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); + }); + + before(async () => { + await kibanaServer.uiSettings.update({ + 'visualization:visualize:legacyPieChartsLibrary': false, + 'visualization:visualize:legacyHeatmapChartsLibrary': false, + }); + await browser.refresh(); + }); + + after(async () => { + await kibanaServer.uiSettings.update({ + 'visualization:visualize:legacyPieChartsLibrary': true, + 'visualization:visualize:legacyHeatmapChartsLibrary': true, + }); + await browser.refresh(); + }); + + // Test replaced vislib chart types + loadTestFile(require.resolve('./_area_chart')); + loadTestFile(require.resolve('./_line_chart_split_series')); + loadTestFile(require.resolve('./_line_chart_split_chart')); + loadTestFile(require.resolve('./_point_series_options')); + loadTestFile(require.resolve('./_vertical_bar_chart')); + loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex')); + loadTestFile(require.resolve('./_timelion')); + loadTestFile(require.resolve('../group3/_pie_chart')); + loadTestFile(require.resolve('../group2/_heatmap_chart')); + }); +} diff --git a/test/functional/config.js b/test/functional/config.base.js similarity index 95% rename from test/functional/config.js rename to test/functional/config.base.js index 9221bef808e01..40b50da505951 100644 --- a/test/functional/config.js +++ b/test/functional/config.base.js @@ -13,20 +13,6 @@ export default async function ({ readConfigFile }) { const commonConfig = await readConfigFile(require.resolve('../common/config')); return { - testFiles: [ - require.resolve('./apps/status_page'), - require.resolve('./apps/bundles'), - require.resolve('./apps/console'), - require.resolve('./apps/context'), - require.resolve('./apps/dashboard'), - require.resolve('./apps/dashboard_elements'), - require.resolve('./apps/discover'), - require.resolve('./apps/getting_started'), - require.resolve('./apps/home'), - require.resolve('./apps/management'), - require.resolve('./apps/saved_objects_management'), - require.resolve('./apps/visualize'), - ], pageObjects, services, diff --git a/test/functional/config.ccs.ts b/test/functional/config.ccs.ts index 8137635b474da..e5a3736d6fbc3 100644 --- a/test/functional/config.ccs.ts +++ b/test/functional/config.ccs.ts @@ -12,15 +12,15 @@ import { RemoteEsProvider } from './services/remote_es/remote_es'; // eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('./config')); + const baseConfig = await readConfigFile(require.resolve('./config.base.js')); return { - ...functionalConfig.getAll(), + ...baseConfig.getAll(), testFiles: [require.resolve('./apps/discover')], services: { - ...functionalConfig.get('services'), + ...baseConfig.get('services'), remoteEs: RemoteEsProvider, remoteEsArchiver: RemoteEsArchiverProvider, }, @@ -30,7 +30,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, security: { - ...functionalConfig.get('security'), + ...baseConfig.get('security'), remoteEsRoles: { ccs_remote_search: { indices: [ @@ -41,17 +41,15 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ], }, }, - defaultRoles: [...(functionalConfig.get('security.defaultRoles') ?? []), 'ccs_remote_search'], + defaultRoles: [...(baseConfig.get('security.defaultRoles') ?? []), 'ccs_remote_search'], }, esTestCluster: { - ...functionalConfig.get('esTestCluster'), + ...baseConfig.get('esTestCluster'), ccs: { remoteClusterUrl: process.env.REMOTE_CLUSTER_URL ?? - `http://elastic:changeme@localhost:${ - functionalConfig.get('servers.elasticsearch.port') + 1 - }`, + `http://elastic:changeme@localhost:${baseConfig.get('servers.elasticsearch.port') + 1}`, }, }, }; diff --git a/test/functional/config.edge.js b/test/functional/config.edge.js index 89dc4c39bca4c..86e213fd31e9f 100644 --- a/test/functional/config.edge.js +++ b/test/functional/config.edge.js @@ -7,10 +7,10 @@ */ export default async function ({ readConfigFile }) { - const defaultConfig = await readConfigFile(require.resolve('./config')); + const firefoxConfig = await readConfigFile(require.resolve('./config.firefox.js')); return { - ...defaultConfig.getAll(), + ...firefoxConfig.getAll(), browser: { type: 'msedge', diff --git a/test/functional/config.firefox.js b/test/functional/config.firefox.js index 145add328c9ca..79a757b1f1116 100644 --- a/test/functional/config.firefox.js +++ b/test/functional/config.firefox.js @@ -7,16 +7,26 @@ */ export default async function ({ readConfigFile }) { - const defaultConfig = await readConfigFile(require.resolve('./config')); + const baseConfig = await readConfigFile(require.resolve('./config.base.js')); return { - ...defaultConfig.getAll(), + ...baseConfig.getAll(), + + testFiles: [ + require.resolve('./apps/console'), + require.resolve('./apps/dashboard/group4/dashboard_save'), + require.resolve('./apps/dashboard_elements'), + require.resolve('./apps/discover'), + require.resolve('./apps/home'), + require.resolve('./apps/visualize/group5'), + ], browser: { type: 'firefox', }, suiteTags: { + include: ['includeFirefox'], exclude: ['skipFirefox'], }, diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 28ac88674b4a6..206cc82912c36 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -281,6 +281,7 @@ export class CommonPageObject extends FtrService { } if (appName === 'discover') { await this.browser.setLocalStorageItem('data.autocompleteFtuePopover', 'true'); + await this.browser.setLocalStorageItem('data.newDataViewMenu', 'true'); } return currentUrl; }); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index ce25370493823..5691b4f5609c7 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -22,6 +22,9 @@ export class DiscoverPageObject extends FtrService { private readonly config = this.ctx.getService('config'); private readonly dataGrid = this.ctx.getService('dataGrid'); private readonly kibanaServer = this.ctx.getService('kibanaServer'); + private readonly queryBar = this.ctx.getService('queryBar'); + + private readonly unifiedSearch = this.ctx.getPageObject('unifiedSearch'); private readonly defaultFindTimeout = this.config.get('timeouts.find'); @@ -365,8 +368,7 @@ export class DiscoverPageObject extends FtrService { public async clickIndexPatternActions() { await this.retry.try(async () => { - await this.testSubjects.click('discoverIndexPatternActions'); - await this.testSubjects.existOrFail('discover-addRuntimeField-popover'); + await this.testSubjects.click('discover-dataView-switch-link'); }); } @@ -494,7 +496,7 @@ export class DiscoverPageObject extends FtrService { } public async selectIndexPattern(indexPattern: string) { - await this.testSubjects.click('indexPattern-switch-link'); + await this.testSubjects.click('discover-dataView-switch-link'); await this.find.setValue('[data-test-subj="indexPattern-switcher"] input', indexPattern); await this.find.clickByCssSelector( `[data-test-subj="indexPattern-switcher"] [title="${indexPattern}"]` @@ -557,6 +559,7 @@ export class DiscoverPageObject extends FtrService { await this.retry.waitFor('Discover app on screen', async () => { return await this.isDiscoverAppOnScreen(); }); + await this.unifiedSearch.closeTourPopoverByLocalStorage(); } public async showAllFilterActions() { @@ -564,10 +567,13 @@ export class DiscoverPageObject extends FtrService { } public async clickSavedQueriesPopOver() { - await this.testSubjects.click('saved-query-management-popover-button'); + await this.testSubjects.click('showQueryBarMenu'); } public async clickCurrentSavedQuery() { + await this.queryBar.setQuery('Cancelled : true'); + await this.queryBar.clickQuerySubmitButton(); + await this.testSubjects.click('showQueryBarMenu'); await this.testSubjects.click('saved-query-management-save-button'); } @@ -630,7 +636,7 @@ export class DiscoverPageObject extends FtrService { public async getCurrentlySelectedDataView() { await this.testSubjects.existOrFail('discover-sidebar'); - const button = await this.testSubjects.find('indexPattern-switch-link'); + const button = await this.testSubjects.find('discover-dataView-switch-link'); return button.getAttribute('title'); } } diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index 1e3e6a9634f4c..4acd8a6e10e95 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -78,6 +78,11 @@ export class HomePageObject extends FtrService { }); } + async launchSampleDiscover(id: string) { + await this.launchSampleDataSet(id); + await this.find.clickByLinkText('Discover'); + } + async launchSampleDashboard(id: string) { await this.launchSampleDataSet(id); await this.find.clickByLinkText('Dashboard'); diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index 826c4b78d1d0f..bdfe91efef900 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -31,6 +31,7 @@ import { SavedObjectsPageObject } from './management/saved_objects_page'; import { LegacyDataTableVisPageObject } from './legacy/data_table_vis'; import { IndexPatternFieldEditorPageObject } from './management/indexpattern_field_editor_page'; import { DashboardPageControls } from './dashboard_page_controls'; +import { UnifiedSearchPageObject } from './unified_search_page'; export const pageObjects = { common: CommonPageObject, @@ -58,4 +59,5 @@ export const pageObjects = { vegaChart: VegaChartPageObject, savedObjects: SavedObjectsPageObject, indexPatternFieldEditorObjects: IndexPatternFieldEditorPageObject, + unifiedSearch: UnifiedSearchPageObject, }; diff --git a/test/functional/page_objects/unified_search_page.ts b/test/functional/page_objects/unified_search_page.ts new file mode 100644 index 0000000000000..b1bcd0662f77e --- /dev/null +++ b/test/functional/page_objects/unified_search_page.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 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 { FtrService } from '../ftr_provider_context'; + +export class UnifiedSearchPageObject extends FtrService { + private readonly browser = this.ctx.getService('browser'); + private readonly testSubjects = this.ctx.getService('testSubjects'); + + public async closeTour() { + const tourPopoverIsOpen = await this.testSubjects.exists('dataViewPickerTourLink'); + if (tourPopoverIsOpen) { + await this.testSubjects.click('dataViewPickerTourLink'); + } + } + + public async closeTourPopoverByLocalStorage() { + await this.browser.setLocalStorageItem('data.newDataViewMenu', 'true'); + await this.browser.refresh(); + } +} diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index fbf6b96b3136d..f96e4088da78f 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -374,11 +374,13 @@ export class VisualBuilderPageObject extends FtrService { } public async getTopNLabel() { + await this.visChart.waitForVisualizationRenderingStabilized(); const topNLabel = await this.find.byCssSelector('.tvbVisTopN__label'); return await topNLabel.getVisibleText(); } public async getTopNCount() { + await this.visChart.waitForVisualizationRenderingStabilized(); const gaugeCount = await this.find.byCssSelector('.tvbVisTopN__value'); return await gaugeCount.getVisibleText(); } diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 20aec8ba5d984..e087d50f21003 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -39,6 +39,7 @@ export class VisualizePageObject extends FtrService { private readonly elasticChart = this.ctx.getService('elasticChart'); private readonly common = this.ctx.getPageObject('common'); private readonly header = this.ctx.getPageObject('header'); + private readonly unifiedSearch = this.ctx.getPageObject('unifiedSearch'); private readonly visEditor = this.ctx.getPageObject('visEditor'); private readonly visChart = this.ctx.getPageObject('visChart'); @@ -154,6 +155,10 @@ export class VisualizePageObject extends FtrService { public async clickVisType(type: string) { await this.testSubjects.click(`visType-${type}`); await this.header.waitUntilLoadingHasFinished(); + + if (type === 'lens') { + await this.unifiedSearch.closeTour(); + } } public async clickAreaChart() { diff --git a/test/functional/services/common/screenshots.ts b/test/functional/services/common/screenshots.ts index d5f901300941f..5fca603407760 100644 --- a/test/functional/services/common/screenshots.ts +++ b/test/functional/services/common/screenshots.ts @@ -22,7 +22,6 @@ const writeFileAsync = promisify(writeFile); export class ScreenshotsService extends FtrService { private readonly log = this.ctx.getService('log'); private readonly config = this.ctx.getService('config'); - private readonly testMetadata = this.ctx.getService('testMetadata'); private readonly browser = this.ctx.getService('browser'); private readonly SESSION_DIRECTORY = resolve(this.config.get('screenshots.directory'), 'session'); @@ -54,13 +53,7 @@ export class ScreenshotsService extends FtrService { const baselinePath = resolve(this.BASELINE_DIRECTORY, `${name}.png`); const failurePath = resolve(this.FAILURE_DIRECTORY, `${name}.png`); - await this.capture({ - path: sessionPath, - name, - el, - baselinePath, - failurePath, - }); + await this.capture(sessionPath, el); if (updateBaselines) { this.log.debug('Updating baseline snapshot'); @@ -82,42 +75,20 @@ export class ScreenshotsService extends FtrService { async take(name: string, el?: WebElementWrapper, subDirectories: string[] = []) { const path = resolve(this.SESSION_DIRECTORY, ...subDirectories, `${name}.png`); - await this.capture({ path, name, el }); + await this.capture(path, el); } async takeForFailure(name: string, el?: WebElementWrapper) { const path = resolve(this.FAILURE_DIRECTORY, `${name}.png`); - await this.capture({ - path, - name: `failure[${name}]`, - el, - }); + await this.capture(path, el); } - private async capture({ - path, - el, - name, - baselinePath, - failurePath, - }: { - path: string; - name: string; - el?: WebElementWrapper; - baselinePath?: string; - failurePath?: string; - }) { + private async capture(path: string, el?: WebElementWrapper) { try { this.log.info(`Taking screenshot "${path}"`); const screenshot = await (el ? el.takeScreenshot() : this.browser.takeScreenshot()); await mkdirAsync(dirname(path), { recursive: true }); await writeFileAsync(path, screenshot, 'base64'); - this.testMetadata.addScreenshot({ - name, - base64Png: Buffer.isBuffer(screenshot) ? screenshot.toString('base64') : screenshot, - baselinePath, - failurePath, - }); } catch (err) { this.log.error('SCREENSHOT FAILED'); this.log.error(err); diff --git a/test/functional/services/dashboard/visualizations.ts b/test/functional/services/dashboard/visualizations.ts index 8688d375f7a7b..48828798a4efa 100644 --- a/test/functional/services/dashboard/visualizations.ts +++ b/test/functional/services/dashboard/visualizations.ts @@ -17,6 +17,7 @@ export class DashboardVisualizationsService extends FtrService { private readonly visualize = this.ctx.getPageObject('visualize'); private readonly visEditor = this.ctx.getPageObject('visEditor'); private readonly header = this.ctx.getPageObject('header'); + private readonly unifiedSearch = this.ctx.getPageObject('unifiedSearch'); private readonly discover = this.ctx.getPageObject('discover'); private readonly timePicker = this.ctx.getPageObject('timePicker'); @@ -43,6 +44,7 @@ export class DashboardVisualizationsService extends FtrService { }) { this.log.debug(`createSavedSearch(${name})`); await this.header.clickDiscover(true); + await this.unifiedSearch.closeTourPopoverByLocalStorage(); await this.timePicker.setHistoricalDataRange(); if (query) { diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts index eee1a1027f541..7178013d5b9fd 100644 --- a/test/functional/services/filter_bar.ts +++ b/test/functional/services/filter_bar.ts @@ -64,8 +64,8 @@ export class FilterBarService extends FtrService { * Removes all filters */ public async removeAllFilters(): Promise { - await this.testSubjects.click('showFilterActions'); - await this.testSubjects.click('removeAllFilters'); + await this.testSubjects.click('showQueryBarMenu'); + await this.testSubjects.click('filter-sets-removeAllFilters'); await this.header.waitUntilLoadingHasFinished(); await this.common.waitUntilUrlIncludes('filters:!()'); } diff --git a/test/functional/services/query_bar.ts b/test/functional/services/query_bar.ts index ec5fc039101a5..ca6c161accc39 100644 --- a/test/functional/services/query_bar.ts +++ b/test/functional/services/query_bar.ts @@ -16,7 +16,6 @@ export class QueryBarService extends FtrService { private readonly common = this.ctx.getPageObject('common'); private readonly header = this.ctx.getPageObject('header'); private readonly find = this.ctx.getService('find'); - private readonly browser = this.ctx.getService('browser'); async getQueryString(): Promise { return await this.testSubjects.getAttribute('queryInput', 'value'); @@ -60,20 +59,19 @@ export class QueryBarService extends FtrService { public async switchQueryLanguage(lang: 'kql' | 'lucene'): Promise { await this.testSubjects.click('switchQueryLanguageButton'); - const kqlToggle = await this.testSubjects.find('languageToggle'); - const currentLang = - (await kqlToggle.getAttribute('aria-checked')) === 'true' ? 'kql' : 'lucene'; - if (lang !== currentLang) { - await kqlToggle.click(); + await this.testSubjects.click(`${lang}LanguageMenuItem`); + const contextMenuPanelTitleButton = await this.testSubjects.exists( + 'contextMenuPanelTitleButton' + ); + if (contextMenuPanelTitleButton) { + await this.testSubjects.click('contextMenuPanelTitleButton'); } - - await this.browser.pressKeys(this.browser.keys.ESCAPE); // close popover await this.expectQueryLanguageOrFail(lang); // make sure lang is switched } public async expectQueryLanguageOrFail(lang: 'kql' | 'lucene'): Promise { const queryLanguageButton = await this.testSubjects.find('switchQueryLanguageButton'); - expect((await queryLanguageButton.getVisibleText()).toLowerCase()).to.eql(lang); + expect((await queryLanguageButton.getVisibleText()).toLowerCase()).to.eql(`language: ${lang}`); } /** diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index a216f8cb0469e..7822ed8f77a89 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -19,7 +19,7 @@ export class SavedQueryManagementComponentService extends FtrService { public async getCurrentlyLoadedQueryID() { await this.openSavedQueryManagementComponent(); try { - return await this.testSubjects.getVisibleText('~saved-query-list-item-selected'); + return await this.testSubjects.getVisibleText('savedQueryTitle'); } catch { return undefined; } @@ -53,7 +53,12 @@ export class SavedQueryManagementComponentService extends FtrService { return saveQueryFormSaveButtonStatus === false; }); - await this.testSubjects.click('savedQueryFormCancelButton'); + const contextMenuPanelTitleButton = await this.testSubjects.exists( + 'contextMenuPanelTitleButton' + ); + if (contextMenuPanelTitleButton) { + await this.testSubjects.click('contextMenuPanelTitleButton'); + } } public async saveCurrentlyLoadedAsNewQuery( @@ -63,7 +68,7 @@ export class SavedQueryManagementComponentService extends FtrService { includeTimeFilter: boolean ) { await this.openSavedQueryManagementComponent(); - await this.testSubjects.click('saved-query-management-save-as-new-button'); + await this.testSubjects.click('saved-query-management-save-button'); await this.submitSaveQueryForm(name, description, includeFilters, includeTimeFilter); } @@ -79,12 +84,12 @@ export class SavedQueryManagementComponentService extends FtrService { public async loadSavedQuery(title: string) { await this.openSavedQueryManagementComponent(); + await this.testSubjects.click('saved-query-management-load-button'); await this.testSubjects.click(`~load-saved-query-${title}-button`); + await this.testSubjects.click('saved-query-management-apply-changes-button'); await this.retry.try(async () => { await this.openSavedQueryManagementComponent(); - const selectedSavedQueryText = await this.testSubjects.getVisibleText( - '~saved-query-list-item-selected' - ); + const selectedSavedQueryText = await this.testSubjects.getVisibleText('savedQueryTitle'); expect(selectedSavedQueryText).to.eql(title); }); await this.closeSavedQueryManagementComponent(); @@ -92,13 +97,24 @@ export class SavedQueryManagementComponentService extends FtrService { public async deleteSavedQuery(title: string) { await this.openSavedQueryManagementComponent(); - await this.testSubjects.click(`~delete-saved-query-${title}-button`); + const shouldClickLoadMenu = await this.testSubjects.exists( + 'saved-query-management-load-button' + ); + if (shouldClickLoadMenu) { + await this.testSubjects.click('saved-query-management-load-button'); + } + await this.testSubjects.click(`~load-saved-query-${title}-button`); + await this.retry.waitFor('delete saved query', async () => { + await this.testSubjects.click(`delete-saved-query-${title}-button`); + const exists = await this.testSubjects.exists('confirmModalTitleText'); + return exists === true; + }); await this.common.clickConfirmOnModal(); } async clearCurrentlyLoadedQuery() { await this.openSavedQueryManagementComponent(); - await this.testSubjects.click('saved-query-management-clear-button'); + await this.testSubjects.click('filter-sets-removeAllFilters'); await this.closeSavedQueryManagementComponent(); const queryString = await this.queryBar.getQueryString(); expect(queryString).to.be.empty(); @@ -113,7 +129,6 @@ export class SavedQueryManagementComponentService extends FtrService { if (title) { await this.testSubjects.setValue('saveQueryFormTitle', title); } - await this.testSubjects.setValue('saveQueryFormDescription', description); const currentIncludeFiltersValue = (await this.testSubjects.getAttribute( @@ -138,6 +153,7 @@ export class SavedQueryManagementComponentService extends FtrService { async savedQueryExist(title: string) { await this.openSavedQueryManagementComponent(); + await this.testSubjects.click('saved-query-management-load-button'); const exists = await this.testSubjects.exists(`~load-saved-query-${title}-button`); await this.closeSavedQueryManagementComponent(); return exists; @@ -145,6 +161,13 @@ export class SavedQueryManagementComponentService extends FtrService { async savedQueryExistOrFail(title: string) { await this.openSavedQueryManagementComponent(); + await this.retry.waitFor('load saved query', async () => { + const shouldClickLoadMenu = await this.testSubjects.exists( + 'saved-query-management-load-button' + ); + return shouldClickLoadMenu === true; + }); + await this.testSubjects.click('saved-query-management-load-button'); await this.testSubjects.existOrFail(`~load-saved-query-${title}-button`); } @@ -163,24 +186,19 @@ export class SavedQueryManagementComponentService extends FtrService { } async openSavedQueryManagementComponent() { - const isOpenAlready = await this.testSubjects.exists('saved-query-management-popover'); + const isOpenAlready = await this.testSubjects.exists('queryBarMenuPanel'); 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 () => { - const queryText = await this.testSubjects.getVisibleText('saved-query-management-popover'); - return queryText.length > 0; - }); + await this.testSubjects.click('showQueryBarMenu'); } async closeSavedQueryManagementComponent() { - const isOpenAlready = await this.testSubjects.exists('saved-query-management-popover'); + const isOpenAlready = await this.testSubjects.exists('queryBarMenuPanel'); if (!isOpenAlready) return; await this.retry.try(async () => { - await this.testSubjects.click('saved-query-management-popover-button'); - await this.testSubjects.missingOrFail('saved-query-management-popover'); + await this.testSubjects.click('showQueryBarMenu'); + await this.testSubjects.missingOrFail('queryBarMenuPanel'); }); } @@ -197,7 +215,9 @@ export class SavedQueryManagementComponentService extends FtrService { async saveNewQueryMissingOrFail() { await this.openSavedQueryManagementComponent(); - await this.testSubjects.missingOrFail('saved-query-management-save-button'); + const saveFilterSetBtn = await this.testSubjects.find('saved-query-management-save-button'); + const isDisabled = await saveFilterSetBtn.getAttribute('disabled'); + expect(isDisabled).to.equal('true'); } async updateCurrentlyLoadedQueryMissingOrFail() { diff --git a/test/interactive_setup_api_integration/tests/enrollment_flow.ts b/test/interactive_setup_api_integration/tests/enrollment_flow.ts index f35509f49480a..a9cec78e391c1 100644 --- a/test/interactive_setup_api_integration/tests/enrollment_flow.ts +++ b/test/interactive_setup_api_integration/tests/enrollment_flow.ts @@ -20,7 +20,7 @@ export default function (context: FtrProviderContext) { const config = context.getService('config'); describe('Interactive setup APIs - Enrollment flow', function () { - this.tags(['skipCloud', 'ciGroup11']); + this.tags('skipCloud'); let kibanaVerificationCode: string; let elasticsearchCaFingerprint: string; diff --git a/test/interactive_setup_api_integration/tests/manual_configuration_flow.ts b/test/interactive_setup_api_integration/tests/manual_configuration_flow.ts index 94a06363367ad..156834d503e8f 100644 --- a/test/interactive_setup_api_integration/tests/manual_configuration_flow.ts +++ b/test/interactive_setup_api_integration/tests/manual_configuration_flow.ts @@ -19,7 +19,7 @@ export default function (context: FtrProviderContext) { const config = context.getService('config'); describe('Interactive setup APIs - Manual configuration flow', function () { - this.tags(['skipCloud', 'ciGroup11']); + this.tags('skipCloud'); let kibanaVerificationCode: string; let elasticsearchCaCertificate: string; diff --git a/test/interactive_setup_api_integration/tests/manual_configuration_flow_without_tls.ts b/test/interactive_setup_api_integration/tests/manual_configuration_flow_without_tls.ts index a3964c5fd5aa6..6035b6571a1fc 100644 --- a/test/interactive_setup_api_integration/tests/manual_configuration_flow_without_tls.ts +++ b/test/interactive_setup_api_integration/tests/manual_configuration_flow_without_tls.ts @@ -18,7 +18,7 @@ export default function (context: FtrProviderContext) { const config = context.getService('config'); describe('Interactive setup APIs - Manual configuration flow without TLS', function () { - this.tags(['skipCloud', 'ciGroup11']); + this.tags('skipCloud'); let kibanaVerificationCode: string; before(async () => { diff --git a/test/interactive_setup_functional/manual_configuration_without_security.config.ts b/test/interactive_setup_functional/manual_configuration_without_security.config.ts index 953b33d4e2077..48c917e853b5a 100644 --- a/test/interactive_setup_functional/manual_configuration_without_security.config.ts +++ b/test/interactive_setup_functional/manual_configuration_without_security.config.ts @@ -13,7 +13,7 @@ import type { FtrConfigProviderContext } from '@kbn/test'; import { getDataPath } from '@kbn/utils'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); const testEndpointsPlugin = resolve( __dirname, diff --git a/test/interactive_setup_functional/tests/enrollment_token.ts b/test/interactive_setup_functional/tests/enrollment_token.ts index 20d9da6539692..5af4ed0fc3f2e 100644 --- a/test/interactive_setup_functional/tests/enrollment_token.ts +++ b/test/interactive_setup_functional/tests/enrollment_token.ts @@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) { const log = getService('log'); describe('Interactive Setup Functional Tests (Enrollment token)', function () { - this.tags(['skipCloud', 'ciGroup11']); + this.tags('skipCloud'); const elasticsearchConfig = config.get('servers.elasticsearch'); let verificationCode: string; diff --git a/test/interactive_setup_functional/tests/manual_configuration.ts b/test/interactive_setup_functional/tests/manual_configuration.ts index 68c5068acd267..3f41cf0659567 100644 --- a/test/interactive_setup_functional/tests/manual_configuration.ts +++ b/test/interactive_setup_functional/tests/manual_configuration.ts @@ -19,7 +19,7 @@ export default function ({ getService }: FtrProviderContext) { const log = getService('log'); describe('Interactive Setup Functional Tests (Manual configuration)', function () { - this.tags(['skipCloud', 'ciGroup11']); + this.tags('skipCloud'); let verificationCode: string; before(async function () { diff --git a/test/interactive_setup_functional/tests/manual_configuration_without_security.ts b/test/interactive_setup_functional/tests/manual_configuration_without_security.ts index 20f7bac890da4..f8925e4add106 100644 --- a/test/interactive_setup_functional/tests/manual_configuration_without_security.ts +++ b/test/interactive_setup_functional/tests/manual_configuration_without_security.ts @@ -19,7 +19,7 @@ export default function ({ getService, getPageObject }: FtrProviderContext) { const log = getService('log'); describe('Interactive Setup Functional Tests (Manual configuration without Security)', function () { - this.tags(['skipCloud', 'ciGroup11']); + this.tags('skipCloud'); let verificationCode: string; before(async function () { diff --git a/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts b/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts index 04e31b4ae8454..23595150d55a1 100644 --- a/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts +++ b/test/interactive_setup_functional/tests/manual_configuration_without_tls.ts @@ -19,7 +19,7 @@ export default function ({ getService }: FtrProviderContext) { const log = getService('log'); describe('Interactive Setup Functional Tests (Manual configuration without TLS)', function () { - this.tags(['skipCloud', 'ciGroup11']); + this.tags('skipCloud'); let verificationCode: string; before(async function () { diff --git a/test/interpreter_functional/config.ts b/test/interpreter_functional/config.ts index 3f9c846a51429..0f81089433b33 100644 --- a/test/interpreter_functional/config.ts +++ b/test/interpreter_functional/config.ts @@ -11,7 +11,7 @@ import fs from 'fs'; import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); // Find all folders in ./plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(path.resolve(__dirname, 'plugins')); diff --git a/test/new_visualize_flow/config.ts b/test/new_visualize_flow/config.ts index a6bd97464e2d0..381d316fa1ad0 100644 --- a/test/new_visualize_flow/config.ts +++ b/test/new_visualize_flow/config.ts @@ -9,7 +9,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const commonConfig = await readConfigFile(require.resolve('../functional/config.js')); + const commonConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { testFiles: [require.resolve('./index.ts')], diff --git a/test/new_visualize_flow/index.ts b/test/new_visualize_flow/index.ts index 7cb55069d7d9b..35c90edf9447d 100644 --- a/test/new_visualize_flow/index.ts +++ b/test/new_visualize_flow/index.ts @@ -11,7 +11,6 @@ import { FtrProviderContext } from '../functional/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile, getService }: FtrProviderContext) { describe('New Visualize Flow', function () { - this.tags('ciGroup11'); const esArchiver = getService('esArchiver'); before(async () => { await esArchiver.loadIfNeeded( diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index 4a96b2b402898..b2dbc762ab657 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -11,7 +11,7 @@ import path from 'path'; import fs from 'fs'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); // Find all folders in ./plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(path.resolve(__dirname, 'plugins')); diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 32fc3be58d39c..2d87c0575845f 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -165,6 +165,8 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.cloud.deployment_url (string)', 'xpack.cloud.full_story.enabled (boolean)', 'xpack.cloud.full_story.org_id (any)', + // No PII. Just the list of event types we want to forward to FullStory. + 'xpack.cloud.full_story.eventTypesAllowlist (array)', 'xpack.cloud.id (string)', 'xpack.cloud.organization_url (string)', 'xpack.cloud.profile_url (string)', @@ -188,7 +190,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.osquery.savedQueries (boolean)', 'xpack.remote_clusters.ui.enabled (boolean)', /** - * NOTE: The Reporting plugin is currently disabled in functional tests (see test/functional/config.js). + * NOTE: The Reporting plugin is currently disabled in functional tests (see test/functional/config.base.js). * It will be re-enabled once #102552 is completed. */ // 'xpack.reporting.roles.allow (array)', diff --git a/test/server_integration/config.js b/test/server_integration/config.base.js similarity index 97% rename from test/server_integration/config.js rename to test/server_integration/config.base.js index 0ebb5c48033b8..71006c258c423 100644 --- a/test/server_integration/config.js +++ b/test/server_integration/config.base.js @@ -14,7 +14,7 @@ import { export default async function ({ readConfigFile }) { const commonConfig = await readConfigFile(require.resolve('../common/config')); - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { services: { diff --git a/test/server_integration/http/platform/config.status.ts b/test/server_integration/http/platform/config.status.ts index 20ffc917f8244..8be9b24a56817 100644 --- a/test/server_integration/http/platform/config.status.ts +++ b/test/server_integration/http/platform/config.status.ts @@ -20,7 +20,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; */ // eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const httpConfig = await readConfigFile(require.resolve('../../config')); + const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); // Find all folders in __fixtures__/plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(path.resolve(__dirname, '../../__fixtures__/plugins')); @@ -52,6 +52,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...httpConfig.get('kbnTestServer.runOptions'), // Don't wait for Kibana to be completely ready so that we can test the status timeouts wait: /Kibana is now unavailable/, + alwaysUseSource: true, }, }, }; diff --git a/test/server_integration/http/platform/config.ts b/test/server_integration/http/platform/config.ts index f3cdf426e9cbb..028ff67b43022 100644 --- a/test/server_integration/http/platform/config.ts +++ b/test/server_integration/http/platform/config.ts @@ -10,7 +10,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; // eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const httpConfig = await readConfigFile(require.resolve('../../config')); + const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); return { testFiles: [require.resolve('./cache'), require.resolve('./headers')], diff --git a/test/server_integration/http/ssl/config.js b/test/server_integration/http/ssl/config.js index 260ba7424d676..14d9a27a00f44 100644 --- a/test/server_integration/http/ssl/config.js +++ b/test/server_integration/http/ssl/config.js @@ -11,7 +11,7 @@ import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { createKibanaSupertestProvider } from '../../services'; export default async function ({ readConfigFile }) { - const httpConfig = await readConfigFile(require.resolve('../../config')); + const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); const certificateAuthorities = [readFileSync(CA_CERT_PATH)]; return { diff --git a/test/server_integration/http/ssl_redirect/config.js b/test/server_integration/http/ssl_redirect/config.js index aed7f23593820..47568b16bf6ba 100644 --- a/test/server_integration/http/ssl_redirect/config.js +++ b/test/server_integration/http/ssl_redirect/config.js @@ -13,7 +13,7 @@ import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { createKibanaSupertestProvider } from '../../services'; export default async function ({ readConfigFile }) { - const httpConfig = await readConfigFile(require.resolve('../../config')); + const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); const certificateAuthorities = [readFileSync(CA_CERT_PATH)]; const redirectPort = httpConfig.get('servers.kibana.port') + 1234; diff --git a/test/server_integration/http/ssl_with_p12/config.js b/test/server_integration/http/ssl_with_p12/config.js index 5d2de9e9a9b54..51beb51b20ba4 100644 --- a/test/server_integration/http/ssl_with_p12/config.js +++ b/test/server_integration/http/ssl_with_p12/config.js @@ -11,7 +11,7 @@ import { CA_CERT_PATH, KBN_P12_PATH, KBN_P12_PASSWORD } from '@kbn/dev-utils'; import { createKibanaSupertestProvider } from '../../services'; export default async function ({ readConfigFile }) { - const httpConfig = await readConfigFile(require.resolve('../../config')); + const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); const certificateAuthorities = [readFileSync(CA_CERT_PATH)]; return { diff --git a/test/server_integration/http/ssl_with_p12_intermediate/config.js b/test/server_integration/http/ssl_with_p12_intermediate/config.js index ffcb9da0fa047..a6eb6c82485d9 100644 --- a/test/server_integration/http/ssl_with_p12_intermediate/config.js +++ b/test/server_integration/http/ssl_with_p12_intermediate/config.js @@ -11,7 +11,7 @@ import { CA1_CERT_PATH, CA2_CERT_PATH, EE_P12_PATH, EE_P12_PASSWORD } from '../. import { createKibanaSupertestProvider } from '../../services'; export default async function ({ readConfigFile }) { - const httpConfig = await readConfigFile(require.resolve('../../config')); + const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); const certificateAuthorities = [readFileSync(CA1_CERT_PATH), readFileSync(CA2_CERT_PATH)]; return { diff --git a/test/ui_capabilities/newsfeed_err/config.ts b/test/ui_capabilities/newsfeed_err/config.ts index e9548b41b67a0..4b6eb99a5627c 100644 --- a/test/ui_capabilities/newsfeed_err/config.ts +++ b/test/ui_capabilities/newsfeed_err/config.ts @@ -7,22 +7,20 @@ */ import { FtrConfigProviderContext } from '@kbn/test'; -// @ts-ignore untyped module -import getFunctionalConfig from '../../functional/config'; // eslint-disable-next-line import/no-default-export export default async ({ readConfigFile }: FtrConfigProviderContext) => { - const functionalConfig = await getFunctionalConfig({ readConfigFile }); + const baseConfig = await readConfigFile(require.resolve('../../functional/config.base.js')); return { - ...functionalConfig, + ...baseConfig.getAll(), testFiles: [require.resolve('./test')], kbnTestServer: { - ...functionalConfig.kbnTestServer, + ...baseConfig.get('kbnTestServer'), serverArgs: [ - ...functionalConfig.kbnTestServer.serverArgs, + ...baseConfig.get('kbnTestServer.serverArgs'), `--newsfeed.service.pathTemplate=/api/_newsfeed-FTS-external-service-simulators/kibana/crash.json`, ], }, diff --git a/test/ui_capabilities/newsfeed_err/test.ts b/test/ui_capabilities/newsfeed_err/test.ts index 52c1c0644299a..538d790ac5724 100644 --- a/test/ui_capabilities/newsfeed_err/test.ts +++ b/test/ui_capabilities/newsfeed_err/test.ts @@ -15,8 +15,6 @@ export default function uiCapabilitiesTests({ getService, getPageObjects }: FtrP const PageObjects = getPageObjects(['common', 'newsfeed']); describe('Newsfeed icon button handle errors', function () { - this.tags('ciGroup5'); - before(async () => { await PageObjects.newsfeed.resetPage(); }); diff --git a/test/visual_regression/config.ts b/test/visual_regression/config.ts index 3c11a4c2689ba..294848246e7c8 100644 --- a/test/visual_regression/config.ts +++ b/test/visual_regression/config.ts @@ -10,7 +10,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { ...functionalConfig.getAll(), diff --git a/test/visual_regression/tests/discover/index.ts b/test/visual_regression/tests/discover/index.ts index fe634c02400a4..9142a430f963b 100644 --- a/test/visual_regression/tests/discover/index.ts +++ b/test/visual_regression/tests/discover/index.ts @@ -16,8 +16,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); describe('discover app', function () { - this.tags('ciGroup5'); - before(function () { return browser.setWindowSize(SCREEN_WIDTH, 1000); }); diff --git a/test/visual_regression/tests/vega/index.ts b/test/visual_regression/tests/vega/index.ts index 71f22a3058d91..9ab4e199439a4 100644 --- a/test/visual_regression/tests/vega/index.ts +++ b/test/visual_regression/tests/vega/index.ts @@ -16,8 +16,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); describe('vega app', function () { - this.tags('ciGroup5'); - before(function () { return browser.setWindowSize(SCREEN_WIDTH, 1000); }); diff --git a/versions.json b/versions.json index 6dfe620c9fd7e..3223c4e8c62dd 100644 --- a/versions.json +++ b/versions.json @@ -8,16 +8,11 @@ "currentMinor": true }, { - "version": "8.2.0", + "version": "8.2.1", "branch": "8.2", "currentMajor": true, "previousMinor": true }, - { - "version": "8.1.3", - "branch": "8.1", - "currentMajor": true - }, { "version": "7.17.3", "branch": "7.17", diff --git a/x-pack/README.md b/x-pack/README.md index d104dffff3d28..dad469295ca0f 100644 --- a/x-pack/README.md +++ b/x-pack/README.md @@ -20,7 +20,7 @@ For information on testing, see [the Elastic functional test development guide]( #### Running functional tests -The functional UI tests, the API integration tests, and the SAML API integration tests are all run against a live browser, Kibana, and Elasticsearch install. Each set of tests is specified with a unique config that describes how to start the Elasticsearch server, the Kibana server, and what tests to run against them. The sets of tests that exist today are *functional UI tests* ([specified by this config](test/functional/config.js)), *API integration tests* ([specified by this config](test/api_integration/config.ts)), and *SAML API integration tests* ([specified by this config](test/security_api_integration/saml.config.ts)). +The functional UI tests, the API integration tests, and the SAML API integration tests are all run against a live browser, Kibana, and Elasticsearch install. Each set of tests is specified with a unique config that describes how to start the Elasticsearch server, the Kibana server, and what tests to run against them. The sets of tests that exist today are *functional UI tests* ([specified by this config](test/functional/config.base.js)), *API integration tests* ([specified by this config](test/api_integration/config.ts)), and *SAML API integration tests* ([specified by this config](test/security_api_integration/saml.config.ts)). The script runs all sets of tests sequentially like so: * builds Elasticsearch and X-Pack diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index 419babe97c0f4..246c8fa35fc15 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -19,6 +19,7 @@ const createActionsClientMock = () => { update: jest.fn(), getAll: jest.fn(), getBulk: jest.fn(), + getOAuthAccessToken: jest.fn(), execute: jest.fn(), enqueueExecution: jest.fn(), ephemeralEnqueuedExecution: jest.fn(), diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index afee13b8c9bca..787b4e450a9e0 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -18,11 +18,14 @@ import { actionsConfigMock } from './actions_config.mock'; import { getActionsConfigurationUtilities } from './actions_config'; import { licenseStateMock } from './lib/license_state.mock'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; -import { httpServerMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { + httpServerMock, + loggingSystemMock, + elasticsearchServiceMock, + savedObjectsClientMock, +} from '@kbn/core/server/mocks'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; - -import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; import { actionExecutorMock } from './lib/action_executor.mock'; import uuid from 'uuid'; import { ActionsAuthorization } from './authorization/actions_authorization'; @@ -37,6 +40,9 @@ import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/s import { Logger } from '@kbn/core/server'; import { connectorTokenClientMock } from './builtin_action_types/lib/connector_token_client.mock'; import { inMemoryMetricsMock } from './monitoring/in_memory_metrics.mock'; +import { getOAuthJwtAccessToken } from './builtin_action_types/lib/get_oauth_jwt_access_token'; +import { getOAuthClientCredentialsAccessToken } from './builtin_action_types/lib/get_oauth_client_credentials_access_token'; +import { OAuthParams } from './routes/get_oauth_access_token'; jest.mock('@kbn/core/server/saved_objects/service/lib/utils', () => ({ SavedObjectsUtils: { @@ -60,6 +66,13 @@ jest.mock('./authorization/get_authorization_mode_by_source', () => { }; }); +jest.mock('./builtin_action_types/lib/get_oauth_jwt_access_token', () => ({ + getOAuthJwtAccessToken: jest.fn(), +})); +jest.mock('./builtin_action_types/lib/get_oauth_client_credentials_access_token', () => ({ + getOAuthClientCredentialsAccessToken: jest.fn(), +})); + const defaultKibanaIndex = '.kibana'; const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); @@ -73,6 +86,7 @@ const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); const logger = loggingSystemMock.create().get() as jest.Mocked; const mockTaskManager = taskManagerMock.createSetup(); +const configurationUtilities = actionsConfigMock.create(); let actionsClient: ActionsClient; let mockedLicenseState: jest.Mocked; @@ -115,6 +129,10 @@ beforeEach(() => { usageCounter: mockUsageCounter, connectorTokenClient, }); + (getOAuthJwtAccessToken as jest.Mock).mockResolvedValue(`Bearer jwttokentokentoken`); + (getOAuthClientCredentialsAccessToken as jest.Mock).mockResolvedValue( + `Bearer clienttokentokentoken` + ); }); describe('create()', () => { @@ -1274,6 +1292,292 @@ describe('getBulk()', () => { }); }); +describe('getOAuthAccessToken()', () => { + function getOAuthAccessToken( + requestBody: OAuthParams + ): ReturnType { + actionsClient = new ActionsClient({ + actionTypeRegistry, + unsecuredSavedObjectsClient, + scopedClusterClient, + defaultKibanaIndex, + actionExecutor, + executionEnqueuer, + ephemeralExecutionEnqueuer, + request, + authorization: authorization as unknown as ActionsAuthorization, + preconfiguredActions: [ + { + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: {}, + isPreconfigured: true, + isDeprecated: false, + name: 'test', + config: { + foo: 'bar', + }, + }, + ], + connectorTokenClient: connectorTokenClientMock.create(), + }); + return actionsClient.getOAuthAccessToken(requestBody, logger, configurationUtilities); + } + + describe('authorization', () => { + test('ensures user is authorised to get the type of action', async () => { + await getOAuthAccessToken({ + type: 'jwt', + options: { + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update'); + }); + + test('throws when user is not authorised to create the type of action', async () => { + authorization.ensureAuthorized.mockRejectedValue(new Error(`Unauthorized to update actions`)); + + await expect( + getOAuthAccessToken({ + type: 'jwt', + options: { + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to update actions]`); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update'); + }); + }); + + test('throws when tokenUrl is not using http or https', async () => { + await expect( + getOAuthAccessToken({ + type: 'jwt', + options: { + tokenUrl: 'ftp://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Token URL must use http or https]`); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update'); + }); + + test('throws when tokenUrl does not contain hostname', async () => { + await expect( + getOAuthAccessToken({ + type: 'jwt', + options: { + tokenUrl: '/path/to/myfile', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Token URL must contain hostname]`); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update'); + }); + + test('throws when tokenUrl is not in allowed hosts', async () => { + configurationUtilities.ensureUriAllowed.mockImplementationOnce(() => { + throw new Error('URI not allowed'); + }); + + await expect( + getOAuthAccessToken({ + type: 'jwt', + options: { + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: URI not allowed]`); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update'); + expect(configurationUtilities.ensureUriAllowed).toHaveBeenCalledWith( + `https://testurl.service-now.com/oauth_token.do` + ); + }); + + test('calls getOAuthJwtAccessToken when type="jwt"', async () => { + const result = await getOAuthAccessToken({ + type: 'jwt', + options: { + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }); + expect(result).toEqual({ + accessToken: 'Bearer jwttokentokentoken', + }); + expect(getOAuthJwtAccessToken as jest.Mock).toHaveBeenCalledWith({ + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + }); + expect(getOAuthClientCredentialsAccessToken).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + `Successfully retrieved access token using JWT OAuth with tokenUrl https://testurl.service-now.com/oauth_token.do and config {\"clientId\":\"abc\",\"jwtKeyId\":\"def\",\"userIdentifierValue\":\"userA\"}` + ); + }); + + test('calls getOAuthClientCredentialsAccessToken when type="client"', async () => { + const result = await getOAuthAccessToken({ + type: 'client', + options: { + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + scope: 'https://graph.microsoft.com/.default', + config: { + clientId: 'abc', + tenantId: 'def', + }, + secrets: { + clientSecret: 'iamasecret', + }, + }, + }); + expect(result).toEqual({ + accessToken: 'Bearer clienttokentokentoken', + }); + expect(getOAuthClientCredentialsAccessToken as jest.Mock).toHaveBeenCalledWith({ + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'abc', + tenantId: 'def', + }, + secrets: { + clientSecret: 'iamasecret', + }, + }, + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + oAuthScope: 'https://graph.microsoft.com/.default', + }); + expect(getOAuthJwtAccessToken).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + `Successfully retrieved access token using Client Credentials OAuth with tokenUrl https://login.microsoftonline.com/98765/oauth2/v2.0/token, scope https://graph.microsoft.com/.default and config {\"clientId\":\"abc\",\"tenantId\":\"def\"}` + ); + }); + + test('throws when getOAuthJwtAccessToken throws error', async () => { + (getOAuthJwtAccessToken as jest.Mock).mockRejectedValue(new Error(`Something went wrong!`)); + + await expect( + getOAuthAccessToken({ + type: 'jwt', + options: { + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Failed to retrieve access token]`); + + expect(getOAuthJwtAccessToken as jest.Mock).toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + `Failed to retrieve access token using JWT OAuth with tokenUrl https://testurl.service-now.com/oauth_token.do and config {\"clientId\":\"abc\",\"jwtKeyId\":\"def\",\"userIdentifierValue\":\"userA\"} - Something went wrong!` + ); + }); + + test('throws when getOAuthClientCredentialsAccessToken throws error', async () => { + (getOAuthClientCredentialsAccessToken as jest.Mock).mockRejectedValue( + new Error(`Something went wrong!`) + ); + + await expect( + getOAuthAccessToken({ + type: 'client', + options: { + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + scope: 'https://graph.microsoft.com/.default', + config: { + clientId: 'abc', + tenantId: 'def', + }, + secrets: { + clientSecret: 'iamasecret', + }, + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Failed to retrieve access token]`); + + expect(getOAuthClientCredentialsAccessToken as jest.Mock).toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + `Failed to retrieved access token using Client Credentials OAuth with tokenUrl https://login.microsoftonline.com/98765/oauth2/v2.0/token, scope https://graph.microsoft.com/.default and config {\"clientId\":\"abc\",\"tenantId\":\"def\"} - Something went wrong!` + ); + }); +}); + describe('delete()', () => { describe('authorization', () => { test('ensures user is authorised to delete actions', async () => { diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index dacf6de36bd37..89156bb56b51a 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -6,6 +6,7 @@ */ import Boom from '@hapi/boom'; +import url from 'url'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; @@ -18,6 +19,7 @@ import { SavedObject, KibanaRequest, SavedObjectsUtils, + Logger, } from '@kbn/core/server'; import { AuditLogger } from '@kbn/security-plugin/server'; import { RunNowResult } from '@kbn/task-manager-plugin/server'; @@ -46,6 +48,22 @@ import { import { connectorAuditEvent, ConnectorAuditAction } from './lib/audit_events'; import { trackLegacyRBACExemption } from './lib/track_legacy_rbac_exemption'; import { isConnectorDeprecated } from './lib/is_conector_deprecated'; +import { ActionsConfigurationUtilities } from './actions_config'; +import { + OAuthClientCredentialsParams, + OAuthJwtParams, + OAuthParams, +} from './routes/get_oauth_access_token'; +import { + getOAuthJwtAccessToken, + GetOAuthJwtConfig, + GetOAuthJwtSecrets, +} from './builtin_action_types/lib/get_oauth_jwt_access_token'; +import { + getOAuthClientCredentialsAccessToken, + GetOAuthClientCredentialsConfig, + GetOAuthClientCredentialsSecrets, +} from './builtin_action_types/lib/get_oauth_client_credentials_access_token'; // 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. @@ -448,6 +466,98 @@ export class ActionsClient { return actionResults; } + public async getOAuthAccessToken( + { type, options }: OAuthParams, + logger: Logger, + configurationUtilities: ActionsConfigurationUtilities + ) { + // Verify that user has edit access + await this.authorization.ensureAuthorized('update'); + + // Verify that token url is allowed by allowed hosts config + try { + configurationUtilities.ensureUriAllowed(options.tokenUrl); + } catch (err) { + throw Boom.badRequest(err.message); + } + + // Verify that token url contains a hostname and uses https + const parsedUrl = url.parse( + options.tokenUrl, + false /* parseQueryString */, + true /* slashesDenoteHost */ + ); + + if (!parsedUrl.hostname) { + throw Boom.badRequest(`Token URL must contain hostname`); + } + + if (parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') { + throw Boom.badRequest(`Token URL must use http or https`); + } + + let accessToken: string | null = null; + if (type === 'jwt') { + const tokenOpts = options as OAuthJwtParams; + + try { + accessToken = await getOAuthJwtAccessToken({ + logger, + configurationUtilities, + credentials: { + config: tokenOpts.config as GetOAuthJwtConfig, + secrets: tokenOpts.secrets as GetOAuthJwtSecrets, + }, + tokenUrl: tokenOpts.tokenUrl, + }); + + logger.debug( + `Successfully retrieved access token using JWT OAuth with tokenUrl ${ + tokenOpts.tokenUrl + } and config ${JSON.stringify(tokenOpts.config)}` + ); + } catch (err) { + logger.debug( + `Failed to retrieve access token using JWT OAuth with tokenUrl ${ + tokenOpts.tokenUrl + } and config ${JSON.stringify(tokenOpts.config)} - ${err.message}` + ); + throw Boom.badRequest(`Failed to retrieve access token`); + } + } else if (type === 'client') { + const tokenOpts = options as OAuthClientCredentialsParams; + try { + accessToken = await getOAuthClientCredentialsAccessToken({ + logger, + configurationUtilities, + credentials: { + config: tokenOpts.config as GetOAuthClientCredentialsConfig, + secrets: tokenOpts.secrets as GetOAuthClientCredentialsSecrets, + }, + tokenUrl: tokenOpts.tokenUrl, + oAuthScope: tokenOpts.scope, + }); + + logger.debug( + `Successfully retrieved access token using Client Credentials OAuth with tokenUrl ${ + tokenOpts.tokenUrl + }, scope ${tokenOpts.scope} and config ${JSON.stringify(tokenOpts.config)}` + ); + } catch (err) { + logger.debug( + `Failed to retrieved access token using Client Credentials OAuth with tokenUrl ${ + tokenOpts.tokenUrl + }, scope ${tokenOpts.scope} and config ${JSON.stringify(tokenOpts.config)} - ${ + err.message + }` + ); + throw Boom.badRequest(`Failed to retrieve access token`); + } + } + + return { accessToken }; + } + /** * Delete action */ diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index 470e6ce8cdc8e..a6b68d907cb44 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -129,6 +129,17 @@ describe('isUriAllowed', () => { ).toEqual(true); }); + test('returns true for network path references', () => { + const config: ActionsConfig = { + ...defaultActionsConfig, + allowedHosts: ['my-domain.com'], + enabledActionTypes: [], + }; + expect(getActionsConfigurationUtilities(config).isUriAllowed('//my-domain.com/foo')).toEqual( + true + ); + }); + test('throws when the hostname in the requested uri is not in the allowedHosts', () => { const config: ActionsConfig = defaultActionsConfig; expect( diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index 35e08bb5cfe66..49f1d1fd5445e 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -76,7 +76,7 @@ function isAllowed({ allowedHosts }: ActionsConfig, hostname: string | null): bo function isHostnameAllowedInUri(config: ActionsConfig, uri: string): boolean { return pipe( - tryCatch(() => url.parse(uri)), + tryCatch(() => url.parse(uri, false /* parseQueryString */, true /* slashesDenoteHost */)), map((parsedUrl) => parsedUrl.hostname), mapNullable((hostname) => isAllowed(config, hostname)), getOrElse(() => false) diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts index b33a2d17ed9d8..9dde4790c152d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts @@ -20,8 +20,7 @@ export function createJWTAssertion( logger: Logger, privateKey: string, privateKeyPassword: string | null, - reservedClaims: JWTClaims, - customClaims?: Record + reservedClaims: JWTClaims ): string { const { subject, audience, issuer, expireInMilliseconds, keyId } = reservedClaims; const iat = Math.floor(Date.now() / 1000); @@ -34,7 +33,6 @@ export function createJWTAssertion( iss: issuer, // issuer claim identifies the principal that issued the JWT iat, // issued at claim identifies the time at which the JWT was issued exp: iat + (expireInMilliseconds ?? 3600), // expiration time claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing - ...(customClaims ?? {}), }; try { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.test.ts new file mode 100644 index 0000000000000..2efa79cf09c48 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.test.ts @@ -0,0 +1,306 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Logger } from '@kbn/core/server'; +import { asyncForEach } from '@kbn/std'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { actionsConfigMock } from '../../actions_config.mock'; +import { connectorTokenClientMock } from './connector_token_client.mock'; +import { getOAuthClientCredentialsAccessToken } from './get_oauth_client_credentials_access_token'; +import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token'; + +jest.mock('./request_oauth_client_credentials_token', () => ({ + requestOAuthClientCredentialsToken: jest.fn(), +})); + +const logger = loggingSystemMock.create().get() as jest.Mocked; +const configurationUtilities = actionsConfigMock.create(); +const connectorTokenClient = connectorTokenClientMock.create(); + +describe('getOAuthClientCredentialsAccessToken', () => { + const getOAuthClientCredentialsAccessTokenOpts = { + connectorId: '123', + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + tenantId: 'tenantId', + }, + secrets: { + clientSecret: 'clientSecret', + }, + }, + oAuthScope: 'https://graph.microsoft.com/.default', + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + connectorTokenClient, + }; + + beforeEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + }); + + test('uses stored access token if it exists', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: { + id: '1', + connectorId: '123', + tokenType: 'access_token', + token: 'testtokenvalue', + createdAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + 10000000000).toISOString(), + }, + }); + const accessToken = await getOAuthClientCredentialsAccessToken( + getOAuthClientCredentialsAccessTokenOpts + ); + + expect(accessToken).toEqual('testtokenvalue'); + expect(requestOAuthClientCredentialsToken as jest.Mock).not.toHaveBeenCalled(); + }); + + test('creates new assertion if stored access token does not exist', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: null, + }); + (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthClientCredentialsAccessToken( + getOAuthClientCredentialsAccessTokenOpts + ); + + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(requestOAuthClientCredentialsToken as jest.Mock).toHaveBeenCalledWith( + 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + logger, + { + scope: 'https://graph.microsoft.com/.default', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + + configurationUtilities + ); + expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({ + connectorId: '123', + token: null, + newToken: 'access_token brandnewaccesstoken', + expiresInSec: 1000, + deleteExisting: false, + }); + }); + + test('creates new assertion if stored access token exists but is expired', async () => { + const createdAt = new Date().toISOString(); + const expiresAt = new Date(Date.now() - 100).toISOString(); + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: { + id: '1', + connectorId: '123', + tokenType: 'access_token', + token: 'testtokenvalue', + createdAt, + expiresAt, + }, + }); + (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthClientCredentialsAccessToken( + getOAuthClientCredentialsAccessTokenOpts + ); + + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(requestOAuthClientCredentialsToken as jest.Mock).toHaveBeenCalledWith( + 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + logger, + { + scope: 'https://graph.microsoft.com/.default', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + + configurationUtilities + ); + expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({ + connectorId: '123', + token: { + id: '1', + connectorId: '123', + tokenType: 'access_token', + token: 'testtokenvalue', + createdAt, + expiresAt, + }, + newToken: 'access_token brandnewaccesstoken', + expiresInSec: 1000, + deleteExisting: false, + }); + }); + + test('returns null and logs warning if any required fields are missing', async () => { + await asyncForEach(['clientId', 'tenantId'], async (configField: string) => { + const accessToken = await getOAuthClientCredentialsAccessToken({ + ...getOAuthClientCredentialsAccessTokenOpts, + credentials: { + config: { + ...getOAuthClientCredentialsAccessTokenOpts.credentials.config, + [configField]: null, + }, + secrets: getOAuthClientCredentialsAccessTokenOpts.credentials.secrets, + }, + }); + expect(accessToken).toBeNull(); + expect(logger.warn).toHaveBeenCalledWith( + `Missing required fields for requesting OAuth Client Credentials access token` + ); + }); + + await asyncForEach(['clientSecret'], async (secretsField: string) => { + const accessToken = await getOAuthClientCredentialsAccessToken({ + ...getOAuthClientCredentialsAccessTokenOpts, + credentials: { + config: getOAuthClientCredentialsAccessTokenOpts.credentials.config, + secrets: { + ...getOAuthClientCredentialsAccessTokenOpts.credentials.secrets, + [secretsField]: null, + }, + }, + }); + expect(accessToken).toBeNull(); + expect(logger.warn).toHaveBeenCalledWith( + `Missing required fields for requesting OAuth Client Credentials access token` + ); + }); + }); + + test('throws error if requestOAuthClientCredentialsToken throws error', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: null, + }); + (requestOAuthClientCredentialsToken as jest.Mock).mockRejectedValueOnce( + new Error('requestOAuthClientCredentialsToken error!!') + ); + + await expect( + getOAuthClientCredentialsAccessToken(getOAuthClientCredentialsAccessTokenOpts) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"requestOAuthClientCredentialsToken error!!"`); + }); + + test('logs warning if connectorTokenClient.updateOrReplace throws error', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: null, + }); + (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + connectorTokenClient.updateOrReplace.mockRejectedValueOnce(new Error('updateOrReplace error')); + + const accessToken = await getOAuthClientCredentialsAccessToken( + getOAuthClientCredentialsAccessTokenOpts + ); + + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(logger.warn).toHaveBeenCalledWith( + `Not able to update connector token for connectorId: 123 due to error: updateOrReplace error` + ); + }); + + test('gets access token if connectorId is not provided', async () => { + (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthClientCredentialsAccessToken({ + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + tenantId: 'tenantId', + }, + secrets: { + clientSecret: 'clientSecret', + }, + }, + oAuthScope: 'https://graph.microsoft.com/.default', + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + connectorTokenClient, + }); + + expect(connectorTokenClient.get).not.toHaveBeenCalled(); + expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled(); + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(requestOAuthClientCredentialsToken as jest.Mock).toHaveBeenCalledWith( + 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + logger, + { + scope: 'https://graph.microsoft.com/.default', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + + configurationUtilities + ); + }); + + test('gets access token if connectorTokenClient is not provided', async () => { + (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthClientCredentialsAccessToken({ + connectorId: '123', + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + tenantId: 'tenantId', + }, + secrets: { + clientSecret: 'clientSecret', + }, + }, + oAuthScope: 'https://graph.microsoft.com/.default', + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + }); + + expect(connectorTokenClient.get).not.toHaveBeenCalled(); + expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled(); + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(requestOAuthClientCredentialsToken as jest.Mock).toHaveBeenCalledWith( + 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + logger, + { + scope: 'https://graph.microsoft.com/.default', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + + configurationUtilities + ); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.ts new file mode 100644 index 0000000000000..803cce2db7668 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Logger } from '@kbn/core/server'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ConnectorToken, ConnectorTokenClientContract } from '../../types'; +import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token'; + +export interface GetOAuthClientCredentialsConfig { + clientId: string; + tenantId: string; +} + +export interface GetOAuthClientCredentialsSecrets { + clientSecret: string; +} + +interface GetOAuthClientCredentialsAccessTokenOpts { + connectorId?: string; + tokenUrl: string; + oAuthScope: string; + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; + credentials: { + config: GetOAuthClientCredentialsConfig; + secrets: GetOAuthClientCredentialsSecrets; + }; + connectorTokenClient?: ConnectorTokenClientContract; +} + +export const getOAuthClientCredentialsAccessToken = async ({ + connectorId, + logger, + tokenUrl, + oAuthScope, + configurationUtilities, + credentials, + connectorTokenClient, +}: GetOAuthClientCredentialsAccessTokenOpts) => { + const { clientId, tenantId } = credentials.config; + const { clientSecret } = credentials.secrets; + + if (!clientId || !clientSecret || !tenantId) { + logger.warn(`Missing required fields for requesting OAuth Client Credentials access token`); + return null; + } + + let accessToken: string; + let connectorToken: ConnectorToken | null = null; + let hasErrors: boolean = false; + + if (connectorId && connectorTokenClient) { + // Check if there is a token stored for this connector + const { connectorToken: token, hasErrors: errors } = await connectorTokenClient.get({ + connectorId, + }); + connectorToken = token; + hasErrors = errors; + } + + if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) { + // request access token with jwt assertion + const tokenResult = await requestOAuthClientCredentialsToken( + tokenUrl, + logger, + { + scope: oAuthScope, + clientId, + clientSecret, + }, + configurationUtilities + ); + accessToken = `${tokenResult.tokenType} ${tokenResult.accessToken}`; + + // try to update connector_token SO + if (connectorId && connectorTokenClient) { + try { + await connectorTokenClient.updateOrReplace({ + connectorId, + token: connectorToken, + newToken: accessToken, + expiresInSec: tokenResult.expiresIn, + deleteExisting: hasErrors, + }); + } catch (err) { + logger.warn( + `Not able to update connector token for connectorId: ${connectorId} due to error: ${err.message}` + ); + } + } + } else { + // use existing valid token + accessToken = connectorToken.token; + } + return accessToken; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.test.ts new file mode 100644 index 0000000000000..b48456ddd2a8c --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.test.ts @@ -0,0 +1,350 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Logger } from '@kbn/core/server'; +import { asyncForEach } from '@kbn/std'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { actionsConfigMock } from '../../actions_config.mock'; +import { connectorTokenClientMock } from './connector_token_client.mock'; +import { getOAuthJwtAccessToken } from './get_oauth_jwt_access_token'; +import { createJWTAssertion } from './create_jwt_assertion'; +import { requestOAuthJWTToken } from './request_oauth_jwt_token'; + +jest.mock('./create_jwt_assertion', () => ({ + createJWTAssertion: jest.fn(), +})); +jest.mock('./request_oauth_jwt_token', () => ({ + requestOAuthJWTToken: jest.fn(), +})); + +const logger = loggingSystemMock.create().get() as jest.Mocked; +const configurationUtilities = actionsConfigMock.create(); +const connectorTokenClient = connectorTokenClientMock.create(); + +describe('getOAuthJwtAccessToken', () => { + const getOAuthJwtAccessTokenOpts = { + connectorId: '123', + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + jwtKeyId: 'jwtKeyId', + userIdentifierValue: 'userIdentifierValue', + }, + secrets: { + clientSecret: 'clientSecret', + privateKey: 'privateKey', + privateKeyPassword: 'privateKeyPassword', + }, + }, + tokenUrl: 'https://dev23432523.service-now.com/oauth_token.do', + connectorTokenClient, + }; + + beforeEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + }); + + test('uses stored access token if it exists', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: { + id: '1', + connectorId: '123', + tokenType: 'access_token', + token: 'testtokenvalue', + createdAt: new Date().toISOString(), + expiresAt: new Date(Date.now() + 10000000000).toISOString(), + }, + }); + const accessToken = await getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts); + + expect(accessToken).toEqual('testtokenvalue'); + expect(createJWTAssertion as jest.Mock).not.toHaveBeenCalled(); + expect(requestOAuthJWTToken as jest.Mock).not.toHaveBeenCalled(); + }); + + test('creates new assertion if stored access token does not exist', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: null, + }); + (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); + (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts); + + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith( + logger, + 'privateKey', + 'privateKeyPassword', + { + audience: 'clientId', + issuer: 'clientId', + subject: 'userIdentifierValue', + keyId: 'jwtKeyId', + } + ); + expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith( + 'https://dev23432523.service-now.com/oauth_token.do', + { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' }, + logger, + configurationUtilities + ); + expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({ + connectorId: '123', + token: null, + newToken: 'access_token brandnewaccesstoken', + expiresInSec: 1000, + deleteExisting: false, + }); + }); + + test('creates new assertion if stored access token exists but is expired', async () => { + const createdAt = new Date().toISOString(); + const expiresAt = new Date(Date.now() - 100).toISOString(); + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: { + id: '1', + connectorId: '123', + tokenType: 'access_token', + token: 'testtokenvalue', + createdAt, + expiresAt, + }, + }); + (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); + (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts); + + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith( + logger, + 'privateKey', + 'privateKeyPassword', + { + audience: 'clientId', + issuer: 'clientId', + subject: 'userIdentifierValue', + keyId: 'jwtKeyId', + } + ); + expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith( + 'https://dev23432523.service-now.com/oauth_token.do', + { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' }, + logger, + configurationUtilities + ); + expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({ + connectorId: '123', + token: { + id: '1', + connectorId: '123', + tokenType: 'access_token', + token: 'testtokenvalue', + createdAt, + expiresAt, + }, + newToken: 'access_token brandnewaccesstoken', + expiresInSec: 1000, + deleteExisting: false, + }); + }); + + test('returns null and logs warning if any required fields are missing', async () => { + await asyncForEach( + ['clientId', 'jwtKeyId', 'userIdentifierValue'], + async (configField: string) => { + const accessToken = await getOAuthJwtAccessToken({ + ...getOAuthJwtAccessTokenOpts, + credentials: { + config: { ...getOAuthJwtAccessTokenOpts.credentials.config, [configField]: null }, + secrets: getOAuthJwtAccessTokenOpts.credentials.secrets, + }, + }); + expect(accessToken).toBeNull(); + expect(logger.warn).toHaveBeenCalledWith( + `Missing required fields for requesting OAuth JWT access token` + ); + } + ); + + await asyncForEach(['clientSecret', 'privateKey'], async (secretsField: string) => { + const accessToken = await getOAuthJwtAccessToken({ + ...getOAuthJwtAccessTokenOpts, + credentials: { + config: getOAuthJwtAccessTokenOpts.credentials.config, + secrets: { ...getOAuthJwtAccessTokenOpts.credentials.secrets, [secretsField]: null }, + }, + }); + expect(accessToken).toBeNull(); + expect(logger.warn).toHaveBeenCalledWith( + `Missing required fields for requesting OAuth JWT access token` + ); + }); + }); + + test('throws error if createJWTAssertion throws error', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: null, + }); + (createJWTAssertion as jest.Mock).mockImplementationOnce(() => { + throw new Error('createJWTAssertion error!!'); + }); + + await expect( + getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"createJWTAssertion error!!"`); + }); + + test('throws error if requestOAuthJWTToken throws error', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: null, + }); + (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); + (requestOAuthJWTToken as jest.Mock).mockRejectedValueOnce( + new Error('requestOAuthJWTToken error!!') + ); + + await expect( + getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"requestOAuthJWTToken error!!"`); + }); + + test('logs warning if connectorTokenClient.updateOrReplace throws error', async () => { + connectorTokenClient.get.mockResolvedValueOnce({ + hasErrors: false, + connectorToken: null, + }); + (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); + (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + connectorTokenClient.updateOrReplace.mockRejectedValueOnce(new Error('updateOrReplace error')); + + const accessToken = await getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts); + + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(logger.warn).toHaveBeenCalledWith( + `Not able to update connector token for connectorId: 123 due to error: updateOrReplace error` + ); + }); + + test('gets access token if connectorId is not provided', async () => { + (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); + (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthJwtAccessToken({ + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + jwtKeyId: 'jwtKeyId', + userIdentifierValue: 'userIdentifierValue', + }, + secrets: { + clientSecret: 'clientSecret', + privateKey: 'privateKey', + privateKeyPassword: 'privateKeyPassword', + }, + }, + tokenUrl: 'https://dev23432523.service-now.com/oauth_token.do', + connectorTokenClient, + }); + + expect(connectorTokenClient.get).not.toHaveBeenCalled(); + expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled(); + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith( + logger, + 'privateKey', + 'privateKeyPassword', + { + audience: 'clientId', + issuer: 'clientId', + subject: 'userIdentifierValue', + keyId: 'jwtKeyId', + } + ); + expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith( + 'https://dev23432523.service-now.com/oauth_token.do', + { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' }, + logger, + configurationUtilities + ); + }); + + test('gets access token if connectorTokenClient is not provided', async () => { + (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); + (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ + tokenType: 'access_token', + accessToken: 'brandnewaccesstoken', + expiresIn: 1000, + }); + + const accessToken = await getOAuthJwtAccessToken({ + connectorId: '123', + logger, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + jwtKeyId: 'jwtKeyId', + userIdentifierValue: 'userIdentifierValue', + }, + secrets: { + clientSecret: 'clientSecret', + privateKey: 'privateKey', + privateKeyPassword: 'privateKeyPassword', + }, + }, + tokenUrl: 'https://dev23432523.service-now.com/oauth_token.do', + }); + + expect(connectorTokenClient.get).not.toHaveBeenCalled(); + expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled(); + expect(accessToken).toEqual('access_token brandnewaccesstoken'); + expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith( + logger, + 'privateKey', + 'privateKeyPassword', + { + audience: 'clientId', + issuer: 'clientId', + subject: 'userIdentifierValue', + keyId: 'jwtKeyId', + } + ); + expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith( + 'https://dev23432523.service-now.com/oauth_token.do', + { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' }, + logger, + configurationUtilities + ); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.ts new file mode 100644 index 0000000000000..a4867d99556e7 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Logger } from '@kbn/core/server'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ConnectorToken, ConnectorTokenClientContract } from '../../types'; +import { createJWTAssertion } from './create_jwt_assertion'; +import { requestOAuthJWTToken } from './request_oauth_jwt_token'; + +export interface GetOAuthJwtConfig { + clientId: string; + jwtKeyId: string; + userIdentifierValue: string; +} + +export interface GetOAuthJwtSecrets { + clientSecret: string; + privateKey: string; + privateKeyPassword: string | null; +} + +interface GetOAuthJwtAccessTokenOpts { + connectorId?: string; + tokenUrl: string; + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; + credentials: { + config: GetOAuthJwtConfig; + secrets: GetOAuthJwtSecrets; + }; + connectorTokenClient?: ConnectorTokenClientContract; +} + +export const getOAuthJwtAccessToken = async ({ + connectorId, + logger, + tokenUrl, + configurationUtilities, + credentials, + connectorTokenClient, +}: GetOAuthJwtAccessTokenOpts) => { + const { clientId, jwtKeyId, userIdentifierValue } = credentials.config; + const { clientSecret, privateKey, privateKeyPassword } = credentials.secrets; + + if (!clientId || !clientSecret || !jwtKeyId || !privateKey || !userIdentifierValue) { + logger.warn(`Missing required fields for requesting OAuth JWT access token`); + return null; + } + + let accessToken: string; + let connectorToken: ConnectorToken | null = null; + let hasErrors: boolean = false; + + if (connectorId && connectorTokenClient) { + // Check if there is a token stored for this connector + const { connectorToken: token, hasErrors: errors } = await connectorTokenClient.get({ + connectorId, + }); + connectorToken = token; + hasErrors = errors; + } + + if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) { + // generate a new assertion + const assertion = createJWTAssertion(logger, privateKey, privateKeyPassword, { + audience: clientId, + issuer: clientId, + subject: userIdentifierValue, + keyId: jwtKeyId, + }); + + // request access token with jwt assertion + const tokenResult = await requestOAuthJWTToken( + tokenUrl, + { + clientId, + clientSecret, + assertion, + }, + logger, + configurationUtilities + ); + accessToken = `${tokenResult.tokenType} ${tokenResult.accessToken}`; + + // try to update connector_token SO + if (connectorId && connectorTokenClient) { + try { + await connectorTokenClient.updateOrReplace({ + connectorId, + token: connectorToken, + newToken: accessToken, + expiresInSec: tokenResult.expiresIn, + deleteExisting: hasErrors, + }); + } catch (err) { + logger.warn( + `Not able to update connector token for connectorId: ${connectorId} due to error: ${err.message}` + ); + } + } + } else { + // use existing valid token + accessToken = connectorToken.token; + } + return accessToken; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index 1d1c2c46cb0e4..fbf0d90541659 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -12,8 +12,8 @@ jest.mock('nodemailer', () => ({ jest.mock('./send_email_graph_api', () => ({ sendEmailGraphApi: jest.fn(), })); -jest.mock('./request_oauth_client_credentials_token', () => ({ - requestOAuthClientCredentialsToken: jest.fn(), +jest.mock('./get_oauth_client_credentials_access_token', () => ({ + getOAuthClientCredentialsAccessToken: jest.fn(), })); import { Logger } from '@kbn/core/server'; @@ -24,10 +24,9 @@ import { ProxySettings } from '../../types'; import { actionsConfigMock } from '../../actions_config.mock'; import { CustomHostSettings } from '../../config'; import { sendEmailGraphApi } from './send_email_graph_api'; -import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token'; +import { getOAuthClientCredentialsAccessToken } from './get_oauth_client_credentials_access_token'; import { ConnectorTokenClient } from './connector_token_client'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; -import { connectorTokenClientMock } from './connector_token_client.mock'; const createTransportMock = nodemailer.createTransport as jest.Mock; const sendMailMockResult = { result: 'does not matter' }; @@ -92,314 +91,38 @@ describe('send_email module', () => { test('uses OAuth 2.0 Client Credentials authentication for email using "exchange_server" service', async () => { const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock; - const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock; + const getOAuthClientCredentialsAccessTokenMock = + getOAuthClientCredentialsAccessToken as jest.Mock; const sendEmailOptions = getSendEmailOptions({ transport: { service: 'exchange_server', clientId: '123456', + tenantId: '98765', clientSecret: 'sdfhkdsjhfksdjfh', }, }); - requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({ - tokenType: 'Bearer', - accessToken: 'dfjsdfgdjhfgsjdf', - expiresIn: 123, - }); + getOAuthClientCredentialsAccessTokenMock.mockReturnValueOnce(`Bearer dfjsdfgdjhfgsjdf`); const date = new Date(); date.setDate(date.getDate() + 5); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'connector_token', - references: [], - attributes: { - connectorId: '123', - expiresAt: date.toISOString(), - tokenType: 'access_token', - token: '11111111', - }, - }); - sendEmailGraphApiMock.mockReturnValue({ status: 202, }); - unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ - total: 0, - saved_objects: [], - per_page: 500, - page: 1, - }); await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient); - requestOAuthClientCredentialsTokenMock.mock.calls[0].pop(); - expect(requestOAuthClientCredentialsTokenMock.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "https://login.microsoftonline.com/undefined/oauth2/v2.0/token", - Object { - "context": Array [], - "debug": [MockFunction], - "error": [MockFunction], - "fatal": [MockFunction], - "get": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction], - }, - Object { - "clientId": "123456", - "clientSecret": "sdfhkdsjhfksdjfh", - "scope": "https://graph.microsoft.com/.default", - }, - ] - `); - - delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; - sendEmailGraphApiMock.mock.calls[0].pop(); - expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "graphApiUrl": undefined, - "headers": Object { - "Authorization": "Bearer dfjsdfgdjhfgsjdf", - "Content-Type": "application/json", - }, - "messageHTML": "

a message

- ", - "options": Object { - "connectorId": "1", - "content": Object { - "message": "a message", - "subject": "a subject", - }, - "hasAuth": true, - "routing": Object { - "bcc": Array [], - "cc": Array [ - "bob@example.com", - "robert@example.com", - ], - "from": "fred@example.com", - "to": Array [ - "jim@example.com", - ], - }, - "transport": Object { - "clientId": "123456", - "clientSecret": "sdfhkdsjhfksdjfh", - "password": "changeme", - "service": "exchange_server", - "user": "elastic", - }, - }, - }, - Object { - "context": Array [], - "debug": [MockFunction], - "error": [MockFunction], - "fatal": [MockFunction], - "get": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction], - }, - ] - `); - - expect(unsecuredSavedObjectsClient.create.mock.calls.length).toBe(1); - }); - - test('uses existing "access_token" from "connector_token" SO for authentication for email using "exchange_server" service', async () => { - const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock; - const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock; - const sendEmailOptions = getSendEmailOptions({ - transport: { - service: 'exchange_server', - clientId: '123456', - clientSecret: 'sdfhkdsjhfksdjfh', - }, - }); - requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({ - tokenType: 'Bearer', - accessToken: 'dfjsdfgdjhfgsjdf', - expiresIn: 123, - }); - - sendEmailGraphApiMock.mockReturnValue({ - status: 202, - }); - const date = new Date(); - date.setDate(date.getDate() + 5); - - unsecuredSavedObjectsClient.checkConflicts.mockResolvedValueOnce({ - errors: [], - }); - unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '1', - score: 1, - type: 'connector_token', - references: [], - attributes: { - connectorId: '123', - expiresAt: date.toISOString(), - tokenType: 'access_token', - }, - }, - ], - per_page: 500, - page: 1, - }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'connector_token', - references: [], - attributes: { - token: '11111111', + expect(getOAuthClientCredentialsAccessTokenMock).toHaveBeenCalledWith({ + configurationUtilities: sendEmailOptions.configurationUtilities, + connectorId: '1', + connectorTokenClient, + credentials: { + config: { clientId: '123456', tenantId: '98765' }, + secrets: { clientSecret: 'sdfhkdsjhfksdjfh' }, }, + logger: mockLogger, + oAuthScope: 'https://graph.microsoft.com/.default', + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', }); - await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient); - expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(0); - - delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; - sendEmailGraphApiMock.mock.calls[0].pop(); - expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "graphApiUrl": undefined, - "headers": Object { - "Authorization": "11111111", - "Content-Type": "application/json", - }, - "messageHTML": "

a message

- ", - "options": Object { - "connectorId": "1", - "content": Object { - "message": "a message", - "subject": "a subject", - }, - "hasAuth": true, - "routing": Object { - "bcc": Array [], - "cc": Array [ - "bob@example.com", - "robert@example.com", - ], - "from": "fred@example.com", - "to": Array [ - "jim@example.com", - ], - }, - "transport": Object { - "clientId": "123456", - "clientSecret": "sdfhkdsjhfksdjfh", - "password": "changeme", - "service": "exchange_server", - "user": "elastic", - }, - }, - }, - Object { - "context": Array [], - "debug": [MockFunction], - "error": [MockFunction], - "fatal": [MockFunction], - "get": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction], - }, - ] - `); - - expect(unsecuredSavedObjectsClient.create.mock.calls.length).toBe(0); - }); - - test('request the new token and update existing "access_token" when it is expired for "exchange_server" email service', async () => { - const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock; - const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock; - const sendEmailOptions = getSendEmailOptions({ - transport: { - service: 'exchange_server', - clientId: '123456', - clientSecret: 'sdfhkdsjhfksdjfh', - }, - }); - requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({ - tokenType: 'Bearer', - accessToken: 'dfjsdfgdjhfgsjdf', - expiresIn: 123, - }); - - sendEmailGraphApiMock.mockReturnValue({ - status: 202, - }); - const date = new Date(); - date.setDate(date.getDate() - 5); - - unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ - total: 2, - saved_objects: [ - { - id: '1', - score: 1, - type: 'connector_token', - references: [], - attributes: { - connectorId: '123', - expiresAt: date.toISOString(), - tokenType: 'access_token', - }, - }, - ], - per_page: 500, - page: 1, - }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'connector_token', - references: [], - attributes: { - token: '11111111', - }, - }); - - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'connector_token', - references: [], - attributes: { - connectorId: '123', - expiresAt: date.toISOString(), - tokenType: 'access_token', - token: '11111111', - }, - }); - unsecuredSavedObjectsClient.checkConflicts.mockResolvedValueOnce({ - errors: [], - }); - - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'connector_token', - references: [], - attributes: { - connectorId: '123', - expiresAt: date.toISOString(), - tokenType: 'access_token', - token: '11111111', - }, - }); - - await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient); - expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(1); - delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; sendEmailGraphApiMock.mock.calls[0].pop(); expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` @@ -435,6 +158,7 @@ describe('send_email module', () => { "clientSecret": "sdfhkdsjhfksdjfh", "password": "changeme", "service": "exchange_server", + "tenantId": "98765", "user": "elastic", }, }, @@ -452,209 +176,42 @@ describe('send_email module', () => { }, ] `); - - expect(unsecuredSavedObjectsClient.create.mock.calls.length).toBe(1); }); - test('sending email for "exchange_server" wont fail if connectorTokenClient throw the errors, just log warning message', async () => { + test('throws error if null access token returned when using OAuth 2.0 Client Credentials authentication', async () => { const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock; - const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock; + const getOAuthClientCredentialsAccessTokenMock = + getOAuthClientCredentialsAccessToken as jest.Mock; const sendEmailOptions = getSendEmailOptions({ transport: { service: 'exchange_server', clientId: '123456', + tenantId: '98765', clientSecret: 'sdfhkdsjhfksdjfh', }, }); - requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({ - tokenType: 'Bearer', - accessToken: 'dfjsdfgdjhfgsjdf', - expiresIn: 123, - }); - - sendEmailGraphApiMock.mockReturnValue({ - status: 202, - }); - const date = new Date(); - date.setDate(date.getDate() + 5); - - unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ - total: 0, - saved_objects: [], - per_page: 500, - page: 1, - }); - unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); - - await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient); - expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(1); - expect(unsecuredSavedObjectsClient.create.mock.calls.length).toBe(1); - expect(mockLogger.warn.mock.calls[0]).toMatchObject([ - `Not able to update connector token for connectorId: 1 due to error: Fail`, - ]); + getOAuthClientCredentialsAccessTokenMock.mockReturnValueOnce(null); - delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; - sendEmailGraphApiMock.mock.calls[0].pop(); - expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "graphApiUrl": undefined, - "headers": Object { - "Authorization": "Bearer dfjsdfgdjhfgsjdf", - "Content-Type": "application/json", - }, - "messageHTML": "

a message

- ", - "options": Object { - "connectorId": "1", - "content": Object { - "message": "a message", - "subject": "a subject", - }, - "hasAuth": true, - "routing": Object { - "bcc": Array [], - "cc": Array [ - "bob@example.com", - "robert@example.com", - ], - "from": "fred@example.com", - "to": Array [ - "jim@example.com", - ], - }, - "transport": Object { - "clientId": "123456", - "clientSecret": "sdfhkdsjhfksdjfh", - "password": "changeme", - "service": "exchange_server", - "user": "elastic", - }, - }, - }, - Object { - "context": Array [], - "debug": [MockFunction], - "error": [MockFunction] { - "calls": Array [ - Array [ - "Failed to create connector_token for connectorId \\"1\\" and tokenType: \\"access_token\\". Error: Fail", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - "fatal": [MockFunction], - "get": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction] { - "calls": Array [ - Array [ - "Not able to update connector token for connectorId: 1 due to error: Fail", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, - }, - ] - `); - }); + await expect(() => + sendEmail(mockLogger, sendEmailOptions, connectorTokenClient) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to retrieve access token for connectorId: 1"` + ); - test('delete duplication tokens if connectorTokenClient get method has the errors, like decription error', async () => { - const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock; - const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock; - const sendEmailOptions = getSendEmailOptions({ - transport: { - service: 'exchange_server', - clientId: '123456', - clientSecret: 'sdfhkdsjhfksdjfh', + expect(getOAuthClientCredentialsAccessTokenMock).toHaveBeenCalledWith({ + configurationUtilities: sendEmailOptions.configurationUtilities, + connectorId: '1', + connectorTokenClient, + credentials: { + config: { clientId: '123456', tenantId: '98765' }, + secrets: { clientSecret: 'sdfhkdsjhfksdjfh' }, }, + logger: mockLogger, + oAuthScope: 'https://graph.microsoft.com/.default', + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', }); - requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({ - tokenType: 'Bearer', - accessToken: 'dfjsdfgdjhfgsjdf', - expiresIn: 123, - }); - - sendEmailGraphApiMock.mockReturnValue({ - status: 202, - }); - const date = new Date(); - date.setDate(date.getDate() + 5); - - const connectorTokenClientM = connectorTokenClientMock.create(); - connectorTokenClientM.get.mockResolvedValueOnce({ - hasErrors: true, - connectorToken: null, - }); - - await sendEmail(mockLogger, sendEmailOptions, connectorTokenClientM); - expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(1); - expect(connectorTokenClientM.updateOrReplace.mock.calls.length).toBe(1); - delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities; - sendEmailGraphApiMock.mock.calls[0].pop(); - expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "graphApiUrl": undefined, - "headers": Object { - "Authorization": "Bearer dfjsdfgdjhfgsjdf", - "Content-Type": "application/json", - }, - "messageHTML": "

a message

- ", - "options": Object { - "connectorId": "1", - "content": Object { - "message": "a message", - "subject": "a subject", - }, - "hasAuth": true, - "routing": Object { - "bcc": Array [], - "cc": Array [ - "bob@example.com", - "robert@example.com", - ], - "from": "fred@example.com", - "to": Array [ - "jim@example.com", - ], - }, - "transport": Object { - "clientId": "123456", - "clientSecret": "sdfhkdsjhfksdjfh", - "password": "changeme", - "service": "exchange_server", - "user": "elastic", - }, - }, - }, - Object { - "context": Array [], - "debug": [MockFunction], - "error": [MockFunction], - "fatal": [MockFunction], - "get": [MockFunction], - "info": [MockFunction], - "log": [MockFunction], - "trace": [MockFunction], - "warn": [MockFunction], - }, - ] - `); + expect(sendEmailGraphApiMock).not.toHaveBeenCalled(); }); test('handles unauthenticated email using not secure host/port', async () => { 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 983846adc71e0..f2b059e51e0d6 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 @@ -14,9 +14,9 @@ import { ActionsConfigurationUtilities } from '../../actions_config'; import { CustomHostSettings } from '../../config'; import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options'; import { sendEmailGraphApi } from './send_email_graph_api'; -import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token'; import { ConnectorTokenClientContract, ProxySettings } from '../../types'; import { AdditionalEmailServices } from '../../../common'; +import { getOAuthClientCredentialsAccessToken } from './get_oauth_client_credentials_access_token'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; @@ -86,41 +86,28 @@ async function sendEmailWithExchange( const { transport, configurationUtilities, connectorId } = options; const { clientId, clientSecret, tenantId, oauthTokenUrl } = transport; - let accessToken: string; - - const { connectorToken, hasErrors } = await connectorTokenClient.get({ connectorId }); - if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) { - // request new access token for microsoft exchange online server with Graph API scope - const tokenResult = await requestOAuthClientCredentialsToken( - oauthTokenUrl ?? `${EXCHANGE_ONLINE_SERVER_HOST}/${tenantId}/oauth2/v2.0/token`, - logger, - { - scope: GRAPH_API_OAUTH_SCOPE, - clientId, - clientSecret, + const accessToken = await getOAuthClientCredentialsAccessToken({ + connectorId, + logger, + configurationUtilities, + credentials: { + config: { + clientId: clientId as string, + tenantId: tenantId as string, + }, + secrets: { + clientSecret: clientSecret as string, }, - configurationUtilities - ); - accessToken = `${tokenResult.tokenType} ${tokenResult.accessToken}`; + }, + oAuthScope: GRAPH_API_OAUTH_SCOPE, + tokenUrl: oauthTokenUrl ?? `${EXCHANGE_ONLINE_SERVER_HOST}/${tenantId}/oauth2/v2.0/token`, + connectorTokenClient, + }); - // try to update connector_token SO - try { - await connectorTokenClient.updateOrReplace({ - connectorId, - token: connectorToken, - newToken: accessToken, - expiresInSec: tokenResult.expiresIn, - deleteExisting: hasErrors, - }); - } catch (err) { - logger.warn( - `Not able to update connector token for connectorId: ${connectorId} due to error: ${err.message}` - ); - } - } else { - // use existing valid token - accessToken = connectorToken.token; + if (!accessToken) { + throw new Error(`Unable to retrieve access token for connectorId: ${connectorId}`); } + const headers = { 'Content-Type': 'application/json', Authorization: accessToken, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts index dae4e59728a0c..64a80977709e5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts @@ -14,19 +14,14 @@ import { createServiceError, getPushedDate, throwIfSubActionIsNotSupported, - getAccessToken, getAxiosInstance, } from './utils'; import { connectorTokenClientMock } from '../lib/connector_token_client.mock'; import { actionsConfigMock } from '../../actions_config.mock'; -import { createJWTAssertion } from '../lib/create_jwt_assertion'; -import { requestOAuthJWTToken } from '../lib/request_oauth_jwt_token'; +import { getOAuthJwtAccessToken } from '../lib/get_oauth_jwt_access_token'; -jest.mock('../lib/create_jwt_assertion', () => ({ - createJWTAssertion: jest.fn(), -})); -jest.mock('../lib/request_oauth_jwt_token', () => ({ - requestOAuthJWTToken: jest.fn(), +jest.mock('../lib/get_oauth_jwt_access_token', () => ({ + getOAuthJwtAccessToken: jest.fn(), })); jest.mock('axios', () => ({ @@ -195,7 +190,7 @@ describe('utils', () => { }); }); - test('creates axios instance with interceptor when isOAuth is true and OAuth fields are defined', () => { + test('creates axios instance with interceptor when isOAuth is true and OAuth fields are defined', async () => { connectorTokenClient.get.mockResolvedValueOnce({ hasErrors: false, connectorToken: { @@ -235,206 +230,34 @@ describe('utils', () => { expect(createAxiosInstanceMock).toHaveBeenCalledTimes(1); expect(createAxiosInstanceMock).toHaveBeenCalledWith(); expect(axiosInstanceMock.interceptors.request.use).toHaveBeenCalledTimes(1); - }); - }); - describe('getAccessToken', () => { - const getAccessTokenOpts = { - connectorId: '123', - logger, - configurationUtilities, - credentials: { - config: { - apiUrl: 'https://servicenow', - usesTableApi: true, - isOAuth: true, - clientId: 'clientId', - jwtKeyId: 'jwtKeyId', - userIdentifierValue: 'userIdentifierValue', - }, - secrets: { - clientSecret: 'clientSecret', - privateKey: 'privateKey', - privateKeyPassword: 'privateKeyPassword', - username: null, - password: null, - }, - }, - snServiceUrl: 'https://dev23432523.service-now.com', - connectorTokenClient, - }; - beforeEach(() => { - jest.resetAllMocks(); - jest.clearAllMocks(); - }); + (getOAuthJwtAccessToken as jest.Mock).mockResolvedValueOnce('Bearer tokentokentoken'); - test('uses stored access token if it exists', async () => { - connectorTokenClient.get.mockResolvedValueOnce({ - hasErrors: false, - connectorToken: { - id: '1', - connectorId: '123', - tokenType: 'access_token', - token: 'testtokenvalue', - createdAt: new Date().toISOString(), - expiresAt: new Date(Date.now() + 10000000000).toISOString(), - }, - }); - const accessToken = await getAccessToken(getAccessTokenOpts); - - expect(accessToken).toEqual('testtokenvalue'); - expect(createJWTAssertion as jest.Mock).not.toHaveBeenCalled(); - expect(requestOAuthJWTToken as jest.Mock).not.toHaveBeenCalled(); - }); - - test('creates new assertion if stored access token does not exist', async () => { - connectorTokenClient.get.mockResolvedValueOnce({ - hasErrors: false, - connectorToken: null, - }); - (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); - (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ - tokenType: 'access_token', - accessToken: 'brandnewaccesstoken', - expiresIn: 1000, + const mockRequestCallback = (axiosInstanceMock.interceptors.request.use as jest.Mock).mock + .calls[0][0]; + expect(await mockRequestCallback({ headers: {} })).toEqual({ + headers: { Authorization: 'Bearer tokentokentoken' }, }); - const accessToken = await getAccessToken(getAccessTokenOpts); - - expect(accessToken).toEqual('access_token brandnewaccesstoken'); - expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith( - logger, - 'privateKey', - 'privateKeyPassword', - { - audience: 'clientId', - issuer: 'clientId', - subject: 'userIdentifierValue', - keyId: 'jwtKeyId', - } - ); - expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith( - 'https://dev23432523.service-now.com/oauth_token.do', - { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' }, - logger, - configurationUtilities - ); - expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({ + expect(getOAuthJwtAccessToken as jest.Mock).toHaveBeenCalledWith({ connectorId: '123', - token: null, - newToken: 'access_token brandnewaccesstoken', - expiresInSec: 1000, - deleteExisting: false, - }); - }); - - test('creates new assertion if stored access token exists but is expired', async () => { - const createdAt = new Date().toISOString(); - const expiresAt = new Date(Date.now() - 100).toISOString(); - connectorTokenClient.get.mockResolvedValueOnce({ - hasErrors: false, - connectorToken: { - id: '1', - connectorId: '123', - tokenType: 'access_token', - token: 'testtokenvalue', - createdAt, - expiresAt, - }, - }); - (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); - (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ - tokenType: 'access_token', - accessToken: 'brandnewaccesstoken', - expiresIn: 1000, - }); - - const accessToken = await getAccessToken(getAccessTokenOpts); - - expect(accessToken).toEqual('access_token brandnewaccesstoken'); - expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith( - logger, - 'privateKey', - 'privateKeyPassword', - { - audience: 'clientId', - issuer: 'clientId', - subject: 'userIdentifierValue', - keyId: 'jwtKeyId', - } - ); - expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith( - 'https://dev23432523.service-now.com/oauth_token.do', - { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' }, logger, - configurationUtilities - ); - expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({ - connectorId: '123', - token: { - id: '1', - connectorId: '123', - tokenType: 'access_token', - token: 'testtokenvalue', - createdAt, - expiresAt, + configurationUtilities, + credentials: { + config: { + clientId: 'clientId', + jwtKeyId: 'jwtKeyId', + userIdentifierValue: 'userIdentifierValue', + }, + secrets: { + clientSecret: 'clientSecret', + privateKey: 'privateKey', + privateKeyPassword: null, + }, }, - newToken: 'access_token brandnewaccesstoken', - expiresInSec: 1000, - deleteExisting: false, - }); - }); - - test('throws error if createJWTAssertion throws error', async () => { - connectorTokenClient.get.mockResolvedValueOnce({ - hasErrors: false, - connectorToken: null, - }); - (createJWTAssertion as jest.Mock).mockImplementationOnce(() => { - throw new Error('createJWTAssertion error!!'); - }); - - await expect(getAccessToken(getAccessTokenOpts)).rejects.toThrowErrorMatchingInlineSnapshot( - `"createJWTAssertion error!!"` - ); - }); - - test('throws error if requestOAuthJWTToken throws error', async () => { - connectorTokenClient.get.mockResolvedValueOnce({ - hasErrors: false, - connectorToken: null, - }); - (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); - (requestOAuthJWTToken as jest.Mock).mockRejectedValueOnce( - new Error('requestOAuthJWTToken error!!') - ); - - await expect(getAccessToken(getAccessTokenOpts)).rejects.toThrowErrorMatchingInlineSnapshot( - `"requestOAuthJWTToken error!!"` - ); - }); - - test('logs warning if connectorTokenClient.updateOrReplace throws error', async () => { - connectorTokenClient.get.mockResolvedValueOnce({ - hasErrors: false, - connectorToken: null, - }); - (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion'); - (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({ - tokenType: 'access_token', - accessToken: 'brandnewaccesstoken', - expiresIn: 1000, + tokenUrl: 'https://dev23432523.service-now.com/oauth_token.do', + connectorTokenClient, }); - connectorTokenClient.updateOrReplace.mockRejectedValueOnce( - new Error('updateOrReplace error') - ); - - const accessToken = await getAccessToken(getAccessTokenOpts); - - expect(accessToken).toEqual('access_token brandnewaccesstoken'); - expect(logger.warn).toHaveBeenCalledWith( - `Not able to update ServiceNow connector token for connectorId: 123 due to error: updateOrReplace error` - ); }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts index 84d6741398bce..538967269b1ea 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts @@ -21,8 +21,7 @@ import { addTimeZoneToDate, getErrorMessage } from '../lib/axios_utils'; import * as i18n from './translations'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { ConnectorTokenClientContract } from '../../types'; -import { createJWTAssertion } from '../lib/create_jwt_assertion'; -import { requestOAuthJWTToken } from '../lib/request_oauth_jwt_token'; +import { getOAuthJwtAccessToken } from '../lib/get_oauth_jwt_access_token'; export const prepareIncident = (useOldApi: boolean, incident: PartialIncident): PartialIncident => useOldApi @@ -83,13 +82,13 @@ export const throwIfSubActionIsNotSupported = ({ } }; -export interface GetAccessTokenAndAxiosInstanceOpts { - connectorId: string; +export interface GetAxiosInstanceOpts { + connectorId?: string; logger: Logger; configurationUtilities: ActionsConfigurationUtilities; credentials: ExternalServiceCredentials; snServiceUrl: string; - connectorTokenClient: ConnectorTokenClientContract; + connectorTokenClient?: ConnectorTokenClientContract; } export const getAxiosInstance = ({ @@ -99,7 +98,7 @@ export const getAxiosInstance = ({ credentials, snServiceUrl, connectorTokenClient, -}: GetAccessTokenAndAxiosInstanceOpts): AxiosInstance => { +}: GetAxiosInstanceOpts): AxiosInstance => { const { config, secrets } = credentials; const { isOAuth } = config as ServiceNowPublicConfigurationType; const { username, password } = secrets as ServiceNowSecretConfigurationType; @@ -114,15 +113,25 @@ export const getAxiosInstance = ({ axiosInstance = axios.create(); axiosInstance.interceptors.request.use( async (axiosConfig: AxiosRequestConfig) => { - const accessToken = await getAccessToken({ + const accessToken = await getOAuthJwtAccessToken({ connectorId, logger, configurationUtilities, credentials: { - config: config as ServiceNowPublicConfigurationType, - secrets, + config: { + clientId: config.clientId as string, + jwtKeyId: config.jwtKeyId as string, + userIdentifierValue: config.userIdentifierValue as string, + }, + secrets: { + clientSecret: secrets.clientSecret as string, + privateKey: secrets.privateKey as string, + privateKeyPassword: secrets.privateKeyPassword + ? (secrets.privateKeyPassword as string) + : null, + }, }, - snServiceUrl, + tokenUrl: `${snServiceUrl}/oauth_token.do`, connectorTokenClient, }); axiosConfig.headers.Authorization = accessToken; @@ -136,75 +145,3 @@ export const getAxiosInstance = ({ return axiosInstance; }; - -export const getAccessToken = async ({ - connectorId, - logger, - configurationUtilities, - credentials, - snServiceUrl, - connectorTokenClient, -}: GetAccessTokenAndAxiosInstanceOpts) => { - const { isOAuth, clientId, jwtKeyId, userIdentifierValue } = - credentials.config as ServiceNowPublicConfigurationType; - const { clientSecret, privateKey, privateKeyPassword } = - credentials.secrets as ServiceNowSecretConfigurationType; - - let accessToken: string; - - // Check if there is a token stored for this connector - const { connectorToken, hasErrors } = await connectorTokenClient.get({ connectorId }); - - if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) { - // generate a new assertion - if ( - !isOAuth || - !clientId || - !clientSecret || - !jwtKeyId || - !privateKey || - !userIdentifierValue - ) { - return null; - } - - const assertion = createJWTAssertion(logger, privateKey, privateKeyPassword, { - audience: clientId, - issuer: clientId, - subject: userIdentifierValue, - keyId: jwtKeyId, - }); - - // request access token with jwt assertion - const tokenResult = await requestOAuthJWTToken( - `${snServiceUrl}/oauth_token.do`, - { - clientId, - clientSecret, - assertion, - }, - logger, - configurationUtilities - ); - accessToken = `${tokenResult.tokenType} ${tokenResult.accessToken}`; - - // try to update connector_token SO - try { - await connectorTokenClient.updateOrReplace({ - connectorId, - token: connectorToken, - newToken: accessToken, - expiresInSec: tokenResult.expiresIn, - deleteExisting: hasErrors, - }); - } catch (err) { - logger.warn( - `Not able to update ServiceNow connector token for connectorId: ${connectorId} due to error: ${err.message}` - ); - } - } else { - // use existing valid token - accessToken = connectorToken.token; - } - return accessToken; -}; diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 093236c939aa1..12898cea5a482 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -521,7 +521,7 @@ test('logs a warning when alert executor throws an error', async () => { executorMock.mockRejectedValue(new Error('this action execution is intended to fail')); await actionExecutor.execute(executeParams); expect(loggerMock.warn).toBeCalledWith( - 'action execution failure: test:1: action-1: an error occurred while running the action executor: this action execution is intended to fail' + 'action execution failure: test:1: action-1: an error occurred while running the action: this action execution is intended to fail' ); }); diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index fe77b72f47aa3..b9ed252c6afc2 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -19,6 +19,7 @@ import { validateConnector, } from './validate_with_schema'; import { + ActionType, ActionTypeExecutorResult, ActionTypeRegistryContract, GetServicesFunction, @@ -30,6 +31,7 @@ import { ActionsClient } from '../actions_client'; import { ActionExecutionSource } from './action_execution_source'; import { RelatedSavedObjects } from './related_saved_objects'; import { createActionEventLogRecordObject } from './create_action_event_log_record_object'; +import { ActionExecutionError, ActionExecutionErrorReason } from './errors/action_execution_error'; // 1,000,000 nanoseconds in 1 millisecond const Millis2Nanos = 1000 * 1000; @@ -157,24 +159,6 @@ export class ActionExecutor { } const actionType = actionTypeRegistry.get(actionTypeId); - let validatedParams: Record; - let validatedConfig: Record; - let validatedSecrets: Record; - try { - validatedParams = validateParams(actionType, params); - validatedConfig = validateConfig(actionType, config); - validatedSecrets = validateSecrets(actionType, secrets); - if (actionType.validate?.connector) { - validateConnector(actionType, { - config, - secrets, - }); - } - } catch (err) { - span?.setOutcome('failure'); - return { status: 'error', actionId, message: err.message, retry: false }; - } - const actionLabel = `${actionTypeId}:${actionId}: ${name}`; logger.debug(`executing action ${actionLabel}`); @@ -221,6 +205,14 @@ export class ActionExecutor { let rawResult: ActionTypeExecutorResult; try { + const { validatedParams, validatedConfig, validatedSecrets } = validateAction({ + actionId, + actionType, + params, + config, + secrets, + }); + rawResult = await actionType.executor({ actionId, services, @@ -231,14 +223,19 @@ export class ActionExecutor { taskInfo, }); } catch (err) { - rawResult = { - actionId, - status: 'error', - message: 'an error occurred while running the action executor', - serviceMessage: err.message, - retry: false, - }; + if (err.reason === ActionExecutionErrorReason.Validation) { + rawResult = err.result; + } else { + rawResult = { + actionId, + status: 'error', + message: 'an error occurred while running the action', + serviceMessage: err.message, + retry: false, + }; + } } + eventLogger.stopTiming(event); // allow null-ish return to indicate success @@ -411,3 +408,38 @@ function actionErrorToMessage(result: ActionTypeExecutorResult): string return message; } + +interface ValidateActionOpts { + actionId: string; + actionType: ActionType; + params: Record; + config: unknown; + secrets: unknown; +} + +function validateAction({ actionId, actionType, params, config, secrets }: ValidateActionOpts) { + let validatedParams: Record; + let validatedConfig: Record; + let validatedSecrets: Record; + + try { + validatedParams = validateParams(actionType, params); + validatedConfig = validateConfig(actionType, config); + validatedSecrets = validateSecrets(actionType, secrets); + if (actionType.validate?.connector) { + validateConnector(actionType, { + config, + secrets, + }); + } + + return { validatedParams, validatedConfig, validatedSecrets }; + } catch (err) { + throw new ActionExecutionError(err.message, ActionExecutionErrorReason.Validation, { + actionId, + status: 'error', + message: err.message, + retry: false, + }); + } +} diff --git a/x-pack/plugins/actions/server/lib/errors/action_execution_error.ts b/x-pack/plugins/actions/server/lib/errors/action_execution_error.ts new file mode 100644 index 0000000000000..ad43008ef8e20 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/errors/action_execution_error.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionTypeExecutorResult } from '../../types'; + +export enum ActionExecutionErrorReason { + Validation = 'validation', +} + +export class ActionExecutionError extends Error { + public readonly reason: ActionExecutionErrorReason; + public readonly result: ActionTypeExecutorResult; + + constructor( + message: string, + reason: ActionExecutionErrorReason, + result: ActionTypeExecutorResult + ) { + super(message); + this.reason = reason; + this.result = result; + } +} diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index d89a3c96b01b9..3f3895ec5b69f 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -109,7 +109,7 @@ describe('Actions Plugin', () => { httpServerMock.createKibanaRequest(), httpServerMock.createResponseFactory() )) as unknown as ActionsApiRequestHandlerContext; - actionsContextHandler!.getActionsClient(); + expect(actionsContextHandler!.getActionsClient()).toBeDefined(); }); it('should throw error when ESO plugin is missing encryption key', async () => { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 1fad2a6189693..c097b94a85950 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -311,12 +311,13 @@ export class ActionsPlugin implements Plugin(), - this.licenseState, + defineRoutes({ + router: core.http.createRouter(), + licenseState: this.licenseState, + logger: this.logger, actionsConfigUtils, - this.usageCounter - ); + usageCounter: this.usageCounter, + }); // Cleanup failed execution task definition if (this.actionsConfig.cleanupFailedExecutionsTask.enabled) { diff --git a/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts b/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts new file mode 100644 index 0000000000000..888e87dbdf1f4 --- /dev/null +++ b/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts @@ -0,0 +1,227 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOAuthAccessToken } from './get_oauth_access_token'; +import { Logger } from '@kbn/core/server'; +import { httpServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { mockHandlerArguments } from './legacy/_mock_handler_arguments'; +import { verifyAccessAndContext } from './verify_access_and_context'; +import { actionsConfigMock } from '../actions_config.mock'; +import { actionsClientMock } from '../actions_client.mock'; + +jest.mock('./verify_access_and_context', () => ({ + verifyAccessAndContext: jest.fn(), +})); + +const logger = loggingSystemMock.create().get() as jest.Mocked; +const configurationUtilities = actionsConfigMock.create(); + +beforeEach(() => { + jest.resetAllMocks(); + (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler); +}); + +describe('getOAuthAccessToken', () => { + it('returns jwt access token for given jwt oauth config', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getOAuthAccessToken(router, licenseState, logger, configurationUtilities); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector/_oauth_access_token"`); + + const actionsClient = actionsClientMock.create(); + actionsClient.getOAuthAccessToken.mockResolvedValueOnce({ + accessToken: 'Bearer jwttokentokentoken', + }); + + const requestBody = { + type: 'jwt', + options: { + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }; + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + body: requestBody, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object { + "accessToken": "Bearer jwttokentokentoken", + }, + } + `); + + expect(actionsClient.getOAuthAccessToken).toHaveBeenCalledTimes(1); + expect(actionsClient.getOAuthAccessToken.mock.calls[0]).toEqual([ + requestBody, + logger, + configurationUtilities, + ]); + + expect(res.ok).toHaveBeenCalledWith({ + body: { + accessToken: 'Bearer jwttokentokentoken', + }, + }); + }); + + it('returns client credentials access token for given client credentials oauth config', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getOAuthAccessToken(router, licenseState, logger, configurationUtilities); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector/_oauth_access_token"`); + + const actionsClient = actionsClientMock.create(); + actionsClient.getOAuthAccessToken.mockResolvedValueOnce({ + accessToken: 'Bearer clienttokentokentoken', + }); + + const requestBody = { + type: 'client', + options: { + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + scope: 'https://graph.microsoft.com/.default', + config: { + clientId: 'abc', + tenantId: 'def', + }, + secrets: { + clientSecret: 'iamasecret', + }, + }, + }; + + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + body: requestBody, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object { + "accessToken": "Bearer clienttokentokentoken", + }, + } + `); + + expect(actionsClient.getOAuthAccessToken).toHaveBeenCalledTimes(1); + expect(actionsClient.getOAuthAccessToken.mock.calls[0]).toEqual([ + requestBody, + logger, + configurationUtilities, + ]); + + expect(res.ok).toHaveBeenCalledWith({ + body: { + accessToken: 'Bearer clienttokentokentoken', + }, + }); + }); + + it('ensures the license allows getting servicenow access token', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + getOAuthAccessToken(router, licenseState, logger, configurationUtilities); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector/_oauth_access_token"`); + + const [context, req, res] = mockHandlerArguments( + {}, + { + body: { + type: 'jwt', + options: { + tokenUrl: 'https://testurl.service-now.com/oauth_token.do', + config: { + clientId: 'abc', + jwtKeyId: 'def', + userIdentifierValue: 'userA', + }, + secrets: { + clientSecret: 'iamasecret', + privateKey: 'xyz', + }, + }, + }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); + }); + + it('ensures the license check prevents getting service now access token', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => { + throw new Error('OMG'); + }); + + getOAuthAccessToken(router, licenseState, logger, configurationUtilities); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector/_oauth_access_token"`); + + const [context, req, res] = mockHandlerArguments( + {}, + { + body: { + type: 'client', + options: { + tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token', + scope: 'https://graph.microsoft.com/.default', + config: { + clientId: 'abc', + tenantId: 'def', + }, + secrets: { + clientSecret: 'iamasecret', + }, + }, + }, + }, + ['ok'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function)); + }); +}); diff --git a/x-pack/plugins/actions/server/routes/get_oauth_access_token.ts b/x-pack/plugins/actions/server/routes/get_oauth_access_token.ts new file mode 100644 index 0000000000000..e1b612d321bcd --- /dev/null +++ b/x-pack/plugins/actions/server/routes/get_oauth_access_token.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 { schema, TypeOf } from '@kbn/config-schema'; +import { IRouter, Logger } from '@kbn/core/server'; +import { ILicenseState } from '../lib'; +import { INTERNAL_BASE_ACTION_API_PATH } from '../../common'; +import { ActionsRequestHandlerContext } from '../types'; +import { verifyAccessAndContext } from './verify_access_and_context'; +import { ActionsConfigurationUtilities } from '../actions_config'; + +const oauthJwtBodySchema = schema.object({ + tokenUrl: schema.string(), + config: schema.object({ + clientId: schema.string(), + jwtKeyId: schema.string(), + userIdentifierValue: schema.string(), + }), + secrets: schema.object({ + clientSecret: schema.string(), + privateKey: schema.string(), + privateKeyPassword: schema.maybe(schema.string()), + }), +}); + +export type OAuthJwtParams = TypeOf; + +const oauthClientCredentialsBodySchema = schema.object({ + tokenUrl: schema.string(), + scope: schema.string(), + config: schema.object({ + clientId: schema.string(), + tenantId: schema.string(), + }), + secrets: schema.object({ + clientSecret: schema.string(), + }), +}); + +export type OAuthClientCredentialsParams = TypeOf; + +const bodySchema = schema.object({ + type: schema.oneOf([schema.literal('jwt'), schema.literal('client')]), + options: schema.conditional( + schema.siblingRef('type'), + schema.literal('jwt'), + oauthJwtBodySchema, + oauthClientCredentialsBodySchema + ), +}); + +export type OAuthParams = TypeOf; + +export const getOAuthAccessToken = ( + router: IRouter, + licenseState: ILicenseState, + logger: Logger, + configurationUtilities: ActionsConfigurationUtilities +) => { + router.post( + { + path: `${INTERNAL_BASE_ACTION_API_PATH}/connector/_oauth_access_token`, + validate: { + body: bodySchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const actionsClient = (await context.actions).getActionsClient(); + + return res.ok({ + body: await actionsClient.getOAuthAccessToken(req.body, logger, configurationUtilities), + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/actions/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts index ab90141ae1c80..2822aa3668900 100644 --- a/x-pack/plugins/actions/server/routes/index.ts +++ b/x-pack/plugins/actions/server/routes/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IRouter } from '@kbn/core/server'; +import { IRouter, Logger } from '@kbn/core/server'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; import { ILicenseState } from '../lib'; import { ActionsRequestHandlerContext } from '../types'; @@ -17,15 +17,21 @@ import { getAllActionRoute } from './get_all'; import { connectorTypesRoute } from './connector_types'; import { updateActionRoute } from './update'; import { getWellKnownEmailServiceRoute } from './get_well_known_email_service'; +import { getOAuthAccessToken } from './get_oauth_access_token'; import { defineLegacyRoutes } from './legacy'; import { ActionsConfigurationUtilities } from '../actions_config'; -export function defineRoutes( - router: IRouter, - licenseState: ILicenseState, - actionsConfigUtils: ActionsConfigurationUtilities, - usageCounter?: UsageCounter -) { +export interface RouteOptions { + router: IRouter; + licenseState: ILicenseState; + logger: Logger; + actionsConfigUtils: ActionsConfigurationUtilities; + usageCounter?: UsageCounter; +} + +export function defineRoutes(opts: RouteOptions) { + const { router, licenseState, logger, actionsConfigUtils, usageCounter } = opts; + defineLegacyRoutes(router, licenseState, usageCounter); createActionRoute(router, licenseState); @@ -36,5 +42,6 @@ export function defineRoutes( connectorTypesRoute(router, licenseState); executeActionRoute(router, licenseState); + getOAuthAccessToken(router, licenseState, logger, actionsConfigUtils); getWellKnownEmailServiceRoute(router, licenseState); } diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts index c8f282bf695d7..4509a004c6e58 100644 --- a/x-pack/plugins/alerting/common/rule.ts +++ b/x-pack/plugins/alerting/common/rule.ts @@ -75,6 +75,7 @@ export interface RuleAggregations { ruleEnabledStatus: { enabled: number; disabled: number }; ruleMutedStatus: { muted: number; unmuted: number }; ruleSnoozedStatus: { snoozed: number }; + ruleTags: string[]; } export interface MappedParamsProperties { diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index b9acfed7dcc65..26aa7a706db0a 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -32,6 +32,7 @@ export enum ReadOperations { GetAlertSummary = 'getAlertSummary', GetExecutionLog = 'getExecutionLog', Find = 'find', + GetAuthorizedAlertsIndices = 'getAuthorizedAlertsIndices', } export enum WriteOperations { diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index b342eddaa0c1b..5eba1353df216 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -21,6 +21,7 @@ import { eventLogMock } from '@kbn/event-log-plugin/server/mocks'; import { actionsMock } from '@kbn/actions-plugin/server/mocks'; import { dataPluginMock } from '@kbn/data-plugin/server/mocks'; import { monitoringCollectionMock } from '@kbn/monitoring-collection-plugin/server/mocks'; +import { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server'; import { spacesMock } from '@kbn/spaces-plugin/server/mocks'; const generateAlertingConfig = (): AlertingConfig => ({ @@ -66,6 +67,7 @@ describe('Alerting Plugin', () => { actions: actionsMock.createSetup(), statusService: statusServiceMock.createSetupContract(), monitoringCollection: monitoringCollectionMock.createSetup(), + data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, }; let plugin: AlertingPlugin; @@ -207,6 +209,7 @@ describe('Alerting Plugin', () => { actions: actionsMock.createSetup(), statusService: statusServiceMock.createSetupContract(), monitoringCollection: monitoringCollectionMock.createSetup(), + data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, }); const startContract = plugin.start(coreMock.createStart(), { @@ -246,6 +249,7 @@ describe('Alerting Plugin', () => { actions: actionsMock.createSetup(), statusService: statusServiceMock.createSetupContract(), monitoringCollection: monitoringCollectionMock.createSetup(), + data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, }); const startContract = plugin.start(coreMock.createStart(), { @@ -296,6 +300,7 @@ describe('Alerting Plugin', () => { actions: actionsMock.createSetup(), statusService: statusServiceMock.createSetupContract(), monitoringCollection: monitoringCollectionMock.createSetup(), + data: dataPluginMock.createSetupContract() as unknown as DataPluginSetup, }); const startContract = plugin.start(coreMock.createStart(), { diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 6589b1537f766..063c221ea98db 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -10,6 +10,7 @@ import { BehaviorSubject } from 'rxjs'; import { pick } from 'lodash'; import { UsageCollectionSetup, UsageCounter } from '@kbn/usage-collection-plugin/server'; import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; +import { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server'; import { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart, @@ -140,6 +141,7 @@ export interface AlertingPluginsSetup { eventLog: IEventLogService; statusService: StatusServiceSetup; monitoringCollection: MonitoringCollectionSetup; + data: DataPluginSetup; } export interface AlertingPluginsStart { @@ -247,12 +249,16 @@ export class AlertingPlugin { // Usage counter for telemetry this.usageCounter = plugins.usageCollection?.createUsageCounter(ALERTS_FEATURE_ID); + const getSearchSourceMigrations = plugins.data.search.searchSource.getAllMigrations.bind( + plugins.data.search.searchSource + ); setupSavedObjects( core.savedObjects, plugins.encryptedSavedObjects, this.ruleTypeRegistry, this.logger, - plugins.actions.isPreconfiguredConnector + plugins.actions.isPreconfiguredConnector, + getSearchSourceMigrations ); initializeApiKeyInvalidator( diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts index 7123f1bf4ad6c..8c24b457df565 100644 --- a/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts +++ b/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts @@ -60,6 +60,7 @@ describe('aggregateRulesRoute', () => { ruleSnoozedStatus: { snoozed: 4, }, + ruleTags: ['a', 'b', 'c'], }; rulesClient.aggregate.mockResolvedValueOnce(aggregateResult); @@ -94,6 +95,11 @@ describe('aggregateRulesRoute', () => { "rule_snoozed_status": Object { "snoozed": 4, }, + "rule_tags": Array [ + "a", + "b", + "c", + ], }, } `); @@ -129,6 +135,7 @@ describe('aggregateRulesRoute', () => { rule_snoozed_status: { snoozed: 4, }, + rule_tags: ['a', 'b', 'c'], }, }); }); diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts index 312def72dd65e..c48c74fc28754 100644 --- a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts +++ b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts @@ -50,6 +50,7 @@ const rewriteBodyRes: RewriteResponseCase = ({ ruleEnabledStatus, ruleMutedStatus, ruleSnoozedStatus, + ruleTags, ...rest }) => ({ ...rest, @@ -57,6 +58,7 @@ const rewriteBodyRes: RewriteResponseCase = ({ rule_enabled_status: ruleEnabledStatus, rule_muted_status: ruleMutedStatus, rule_snoozed_status: ruleSnoozedStatus, + rule_tags: ruleTags, }); export const aggregateRulesRoute = ( diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 00f67437ae4f2..e229b15fcd1cd 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -133,6 +133,12 @@ export interface RuleAggregation { doc_count: number; }>; }; + tags: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; } export interface ConstructorOptions { @@ -200,6 +206,7 @@ export interface AggregateResult { ruleEnabledStatus?: { enabled: number; disabled: number }; ruleMutedStatus?: { muted: number; unmuted: number }; ruleSnoozedStatus?: { snoozed: number }; + ruleTags?: string[]; } export interface FindResult { @@ -921,6 +928,9 @@ export class RulesClient { muted: { terms: { field: 'alert.attributes.muteAll' }, }, + tags: { + terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } }, + }, snoozed: { date_range: { field: 'alert.attributes.snoozeEndTime', @@ -990,6 +1000,9 @@ export class RulesClient { snoozed: snoozedBuckets.reduce((acc, bucket) => acc + bucket.doc_count, 0), }; + const tagsBuckets = resp.aggregations.tags?.buckets || []; + ret.ruleTags = tagsBuckets.map((bucket) => bucket.key); + return ret; } diff --git a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts index b74059e4be3d6..1a3d203162bd6 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts @@ -112,6 +112,22 @@ describe('aggregate()', () => { }, ], }, + tags: { + buckets: [ + { + key: 'a', + doc_count: 10, + }, + { + key: 'b', + doc_count: 20, + }, + { + key: 'c', + doc_count: 30, + }, + ], + }, }, }); @@ -160,6 +176,11 @@ describe('aggregate()', () => { "ruleSnoozedStatus": Object { "snoozed": 2, }, + "ruleTags": Array [ + "a", + "b", + "c", + ], } `); expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); @@ -187,6 +208,9 @@ describe('aggregate()', () => { ranges: [{ from: 'now' }], }, }, + tags: { + terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } }, + }, }, }, ]); @@ -221,6 +245,9 @@ describe('aggregate()', () => { ranges: [{ from: 'now' }], }, }, + tags: { + terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } }, + }, }, }, ]); diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index 85e4dc5a8e05b..6566fee15d4a8 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -12,6 +12,7 @@ import type { SavedObjectsServiceSetup, } from '@kbn/core/server'; import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; +import { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common'; import { alertMappings } from './mappings'; import { getMigrations } from './migrations'; import { transformRulesForExport } from './transform_rule_for_export'; @@ -51,14 +52,15 @@ export function setupSavedObjects( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, ruleTypeRegistry: RuleTypeRegistry, logger: Logger, - isPreconfigured: (connectorId: string) => boolean + isPreconfigured: (connectorId: string) => boolean, + getSearchSourceMigrations: () => MigrateFunctionsObject ) { savedObjects.registerType({ name: 'alert', hidden: true, namespaceType: 'multiple-isolated', convertToMultiNamespaceTypeVersion: '8.0.0', - migrations: getMigrations(encryptedSavedObjects, isPreconfigured), + migrations: getMigrations(encryptedSavedObjects, getSearchSourceMigrations(), isPreconfigured), mappings: alertMappings, management: { displayName: 'rule', diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index 921412d4e79e8..c83d0a95dfdcb 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -8,7 +8,7 @@ import uuid from 'uuid'; import { getMigrations, isAnyActionSupportIncidents } from './migrations'; import { RawRule } from '../types'; -import { SavedObjectUnsanitizedDoc } from '@kbn/core/server'; +import { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from '@kbn/core/server'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { migrationMocks } from '@kbn/core/server/mocks'; import { RuleType, ruleTypeMappings } from '@kbn/securitysolution-rules'; @@ -25,7 +25,7 @@ describe('successful migrations', () => { }); describe('7.10.0', () => { test('marks alerts as legacy', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({}); expect(migration710(alert, migrationContext)).toMatchObject({ ...alert, @@ -39,7 +39,7 @@ describe('successful migrations', () => { }); test('migrates the consumer for metrics', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({ consumer: 'metrics', }); @@ -56,7 +56,7 @@ describe('successful migrations', () => { }); test('migrates the consumer for siem', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({ consumer: 'securitySolution', }); @@ -73,7 +73,7 @@ describe('successful migrations', () => { }); test('migrates the consumer for alerting', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({ consumer: 'alerting', }); @@ -90,7 +90,7 @@ describe('successful migrations', () => { }); test('migrates PagerDuty actions to set a default dedupkey of the AlertId', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({ actions: [ { @@ -127,7 +127,7 @@ describe('successful migrations', () => { }); test('skips PagerDuty actions with a specified dedupkey', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({ actions: [ { @@ -165,7 +165,7 @@ describe('successful migrations', () => { }); test('skips PagerDuty actions with an eventAction of "trigger"', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({ actions: [ { @@ -204,7 +204,7 @@ describe('successful migrations', () => { }); test('creates execution status', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData(); const dateStart = Date.now(); const migratedAlert = migration710(alert, migrationContext); @@ -232,7 +232,7 @@ describe('successful migrations', () => { describe('7.11.0', () => { test('add updatedAt field to alert - set to SavedObject updated_at attribute', () => { - const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0']; + const migration711 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.11.0']; const alert = getMockData({}, true); expect(migration711(alert, migrationContext)).toEqual({ ...alert, @@ -245,7 +245,7 @@ describe('successful migrations', () => { }); test('add updatedAt field to alert - set to createdAt when SavedObject updated_at is not defined', () => { - const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0']; + const migration711 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.11.0']; const alert = getMockData({}); expect(migration711(alert, migrationContext)).toEqual({ ...alert, @@ -258,7 +258,7 @@ describe('successful migrations', () => { }); test('add notifyWhen=onActiveAlert when throttle is null', () => { - const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0']; + const migration711 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.11.0']; const alert = getMockData({}); expect(migration711(alert, migrationContext)).toEqual({ ...alert, @@ -271,7 +271,7 @@ describe('successful migrations', () => { }); test('add notifyWhen=onActiveAlert when throttle is set', () => { - const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0']; + const migration711 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.11.0']; const alert = getMockData({ throttle: '5m' }); expect(migration711(alert, migrationContext)).toEqual({ ...alert, @@ -286,7 +286,9 @@ describe('successful migrations', () => { describe('7.11.2', () => { test('transforms connectors that support incident correctly', () => { - const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2']; + const migration7112 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.11.2' + ]; const alert = getMockData({ actions: [ { @@ -428,7 +430,9 @@ describe('successful migrations', () => { }); test('it transforms only subAction=pushToService', () => { - const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2']; + const migration7112 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.11.2' + ]; const alert = getMockData({ actions: [ { @@ -447,7 +451,9 @@ describe('successful migrations', () => { }); test('it does not transforms other connectors', () => { - const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2']; + const migration7112 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.11.2' + ]; const alert = getMockData({ actions: [ { @@ -526,7 +532,9 @@ describe('successful migrations', () => { }); test('it does not transforms alerts when the right structure connectors is already applied', () => { - const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2']; + const migration7112 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.11.2' + ]; const alert = getMockData({ actions: [ { @@ -563,7 +571,9 @@ describe('successful migrations', () => { }); test('if incident attribute is an empty object, copy back the related attributes from subActionParams back to incident', () => { - const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2']; + const migration7112 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.11.2' + ]; const alert = getMockData({ actions: [ { @@ -625,7 +635,9 @@ describe('successful migrations', () => { }); test('custom action does not get migrated/loss', () => { - const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2']; + const migration7112 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.11.2' + ]; const alert = getMockData({ actions: [ { @@ -654,7 +666,7 @@ describe('successful migrations', () => { describe('7.13.0', () => { test('security solution alerts get migrated and remove null values', () => { - const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration713 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.13.0']; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -748,7 +760,7 @@ describe('successful migrations', () => { }); test('non-null values in security solution alerts are not modified', () => { - const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration713 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.13.0']; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -815,7 +827,7 @@ describe('successful migrations', () => { }); test('security solution threshold alert with string in threshold.field is migrated to array', () => { - const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration713 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.13.0']; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -846,7 +858,7 @@ describe('successful migrations', () => { }); test('security solution threshold alert with empty string in threshold.field is migrated to empty array', () => { - const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration713 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.13.0']; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -877,7 +889,7 @@ describe('successful migrations', () => { }); test('security solution threshold alert with array in threshold.field and cardinality is left alone', () => { - const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration713 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.13.0']; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -919,7 +931,7 @@ describe('successful migrations', () => { }); test('security solution ML alert with string in machineLearningJobId is converted to an array', () => { - const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration713 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.13.0']; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -945,7 +957,7 @@ describe('successful migrations', () => { }); test('security solution ML alert with an array in machineLearningJobId is preserved', () => { - const migration713 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration713 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.13.0']; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -973,7 +985,9 @@ describe('successful migrations', () => { describe('7.14.1', () => { test('security solution author field is migrated to array if it is undefined', () => { - const migration7141 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.14.1']; + const migration7141 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.14.1' + ]; const alert = getMockData({ alertTypeId: 'siem.signals', params: {}, @@ -991,7 +1005,9 @@ describe('successful migrations', () => { }); test('security solution author field does not override existing values if they exist', () => { - const migration7141 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.14.1']; + const migration7141 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.14.1' + ]; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -1015,7 +1031,9 @@ describe('successful migrations', () => { describe('7.15.0', () => { test('security solution is migrated to saved object references if it has 1 exceptionsList', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -1044,7 +1062,9 @@ describe('successful migrations', () => { }); test('security solution is migrated to saved object references if it has 2 exceptionsLists', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -1084,7 +1104,9 @@ describe('successful migrations', () => { }); test('security solution is migrated to saved object references if it has 3 exceptionsLists', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -1135,7 +1157,9 @@ describe('successful migrations', () => { }); test('security solution does not change anything if exceptionsList is missing', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = getMockData({ alertTypeId: 'siem.signals', params: { @@ -1147,7 +1171,9 @@ describe('successful migrations', () => { }); test('security solution will keep existing references if we do not have an exceptionsList but we do already have references', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.signals', @@ -1177,7 +1203,9 @@ describe('successful migrations', () => { }); test('security solution keep any foreign references if they exist but still migrate other references', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.signals', @@ -1242,7 +1270,9 @@ describe('successful migrations', () => { }); test('security solution is idempotent and if re-run on the same migrated data will keep the same items', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.signals', @@ -1282,7 +1312,9 @@ describe('successful migrations', () => { }); test('security solution will migrate with only missing data if we have partially migrated data', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.signals', @@ -1331,7 +1363,9 @@ describe('successful migrations', () => { }); test('security solution will not migrate if exception list if it is invalid data', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.signals', @@ -1345,7 +1379,9 @@ describe('successful migrations', () => { }); test('security solution will migrate valid data if it is mixed with invalid data', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.signals', @@ -1387,7 +1423,9 @@ describe('successful migrations', () => { }); test('security solution will not migrate if exception list is invalid data but will keep existing references', () => { - const migration7150 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.15.0']; + const migration7150 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.15.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.signals', @@ -1419,7 +1457,7 @@ describe('successful migrations', () => { describe('7.16.0', () => { test('add legacyId field to alert - set to SavedObject id attribute', () => { - const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration716 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.16.0']; const alert = getMockData({}, true); expect(migration716(alert, migrationContext)).toEqual({ ...alert, @@ -1434,7 +1472,7 @@ describe('successful migrations', () => { isPreconfigured.mockReset(); isPreconfigured.mockReturnValueOnce(true); isPreconfigured.mockReturnValueOnce(false); - const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration716 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.16.0']; const rule = { ...getMockData({ actions: [ @@ -1510,7 +1548,7 @@ describe('successful migrations', () => { isPreconfigured.mockReturnValueOnce(true); isPreconfigured.mockReturnValueOnce(false); isPreconfigured.mockReturnValueOnce(false); - const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration716 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.16.0']; const rule = { ...getMockData({ actions: [ @@ -1593,7 +1631,7 @@ describe('successful migrations', () => { test('does nothing to rules with no references', () => { isPreconfigured.mockReset(); - const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration716 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.16.0']; const rule = { ...getMockData({ actions: [ @@ -1629,7 +1667,7 @@ describe('successful migrations', () => { test('does nothing to rules with no action references', () => { isPreconfigured.mockReset(); - const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration716 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.16.0']; const rule = { ...getMockData({ actions: [ @@ -1671,7 +1709,7 @@ describe('successful migrations', () => { test('does nothing to rules with references but no actions', () => { isPreconfigured.mockReset(); - const migration716 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration716 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.16.0']; const rule = { ...getMockData({ actions: [], @@ -1699,7 +1737,9 @@ describe('successful migrations', () => { }); test('security solution is migrated to saved object references if it has a "ruleAlertId"', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = getMockData({ alertTypeId: 'siem.notifications', params: { @@ -1724,7 +1764,9 @@ describe('successful migrations', () => { }); test('security solution does not migrate anything if its type is not siem.notifications', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = getMockData({ alertTypeId: 'other-type', params: { @@ -1741,7 +1783,9 @@ describe('successful migrations', () => { }); }); test('security solution does not change anything if "ruleAlertId" is missing', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = getMockData({ alertTypeId: 'siem.notifications', params: {}, @@ -1757,7 +1801,9 @@ describe('successful migrations', () => { }); test('security solution will keep existing references if we do not have a "ruleAlertId" but we do already have references', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.notifications', @@ -1789,7 +1835,9 @@ describe('successful migrations', () => { }); test('security solution will keep any foreign references if they exist but still migrate other "ruleAlertId" references', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.notifications', @@ -1828,7 +1876,9 @@ describe('successful migrations', () => { }); test('security solution is idempotent and if re-run on the same migrated data will keep the same items "ruleAlertId" references', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.notifications', @@ -1862,7 +1912,9 @@ describe('successful migrations', () => { }); test('security solution will not migrate "ruleAlertId" if it is invalid data', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.notifications', @@ -1882,7 +1934,9 @@ describe('successful migrations', () => { }); test('security solution will not migrate "ruleAlertId" if it is invalid data but will keep existing references', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: 'siem.notifications', @@ -1916,7 +1970,9 @@ describe('successful migrations', () => { }); test('geo-containment alert migration extracts boundary and index references', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: '.geo-containment', @@ -1944,7 +2000,9 @@ describe('successful migrations', () => { }); test('geo-containment alert migration should preserve foreign references', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: '.geo-containment', @@ -1984,7 +2042,9 @@ describe('successful migrations', () => { }); test('geo-containment alert migration ignores other alert-types', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const alert = { ...getMockData({ alertTypeId: '.foo', @@ -2008,13 +2068,13 @@ describe('successful migrations', () => { describe('8.0.0', () => { test('no op migration for rules SO', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['8.0.0']; const alert = getMockData({}, true); expect(migration800(alert, migrationContext)).toEqual(alert); }); test('add threatIndicatorPath default value to threat match rules if missing', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['8.0.0']; const alert = getMockData( { params: { type: 'threat_match' }, alertTypeId: 'siem.signals' }, true @@ -2025,7 +2085,7 @@ describe('successful migrations', () => { }); test('doesnt change threatIndicatorPath value in threat match rules if value is present', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['8.0.0']; const alert = getMockData( { params: { type: 'threat_match', threatIndicatorPath: 'custom.indicator.path' }, @@ -2039,7 +2099,7 @@ describe('successful migrations', () => { }); test('doesnt change threatIndicatorPath value in other rules', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['8.0.0']; const alert = getMockData({ params: { type: 'eql' }, alertTypeId: 'siem.signals' }, true); expect(migration800(alert, migrationContext).attributes.params.threatIndicatorPath).toEqual( undefined @@ -2047,7 +2107,7 @@ describe('successful migrations', () => { }); test('doesnt change threatIndicatorPath value if not a siem.signals rule', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['8.0.0']; const alert = getMockData( { params: { type: 'threat_match' }, alertTypeId: 'not.siem.signals' }, true @@ -2058,7 +2118,7 @@ describe('successful migrations', () => { }); test('doesnt change AAD rule params if not a siem.signals rule', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['8.0.0']; const alert = getMockData( { params: { outputIndex: 'output-index', type: 'query' }, alertTypeId: 'not.siem.signals' }, true @@ -2073,7 +2133,9 @@ describe('successful migrations', () => { test.each(Object.keys(ruleTypeMappings) as RuleType[])( 'changes AAD rule params accordingly if rule is a siem.signals %p rule', (ruleType) => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.0.0' + ]; const alert = getMockData( { params: { outputIndex: 'output-index', type: ruleType }, alertTypeId: 'siem.signals' }, true @@ -2118,7 +2180,7 @@ describe('successful migrations', () => { ); test('Does not update rule tags if rule has already been enabled', () => { - const migrations = getMigrations(encryptedSavedObjectsSetup, isPreconfigured); + const migrations = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured); const migration800 = migrations['8.0.0']; const migration801 = migrations['8.0.1']; @@ -2141,7 +2203,7 @@ describe('successful migrations', () => { }); test('Does not update rule tags if rule was already disabled before upgrading to 8.0', () => { - const migrations = getMigrations(encryptedSavedObjectsSetup, isPreconfigured); + const migrations = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured); const migration800 = migrations['8.0.0']; const migration801 = migrations['8.0.1']; @@ -2161,7 +2223,7 @@ describe('successful migrations', () => { }); test('Updates rule tags if rule was auto-disabled in 8.0 upgrade and not reenabled', () => { - const migrations = getMigrations(encryptedSavedObjectsSetup, isPreconfigured); + const migrations = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured); const migration800 = migrations['8.0.0']; const migration801 = migrations['8.0.1']; @@ -2181,7 +2243,7 @@ describe('successful migrations', () => { }); test('Updates rule tags correctly if tags are undefined', () => { - const migrations = getMigrations(encryptedSavedObjectsSetup, isPreconfigured); + const migrations = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured); const migration801 = migrations['8.0.1']; const alert = { @@ -2204,7 +2266,7 @@ describe('successful migrations', () => { }); test('Updates rule tags correctly if tags are null', () => { - const migrations = getMigrations(encryptedSavedObjectsSetup, isPreconfigured); + const migrations = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured); const migration801 = migrations['8.0.1']; const alert = { @@ -2231,7 +2293,9 @@ describe('successful migrations', () => { describe('8.2.0', () => { test('migrates params to mapped_params', () => { - const migration820 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.2.0']; + const migration820 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.2.0' + ]; const alert = getMockData( { params: { @@ -2254,8 +2318,29 @@ describe('successful migrations', () => { }); describe('8.3.0', () => { + test('migrates es_query alert params', () => { + const migration830 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.3.0' + ]; + const alert = getMockData( + { + params: { esQuery: '{ "query": "test-query" }' }, + alertTypeId: '.es-query', + }, + true + ); + const migratedAlert820 = migration830(alert, migrationContext); + + expect(migratedAlert820.attributes.params).toEqual({ + esQuery: '{ "query": "test-query" }', + searchType: 'esQuery', + }); + }); + test('removes internal tags', () => { - const migration830 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.3.0']; + const migration830 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.3.0' + ]; const alert = getMockData( { tags: [ @@ -2274,7 +2359,9 @@ describe('successful migrations', () => { }); test('do not remove internal tags if rule is not Security solution rule', () => { - const migration830 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.3.0']; + const migration830 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.3.0' + ]; const alert = getMockData( { tags: ['__internal_immutable:false', 'tag-1'], @@ -2290,7 +2377,9 @@ describe('successful migrations', () => { describe('Metrics Inventory Threshold rule', () => { test('Migrates incorrect action group spelling', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.0.0' + ]; const actions = [ { @@ -2317,7 +2406,9 @@ describe('successful migrations', () => { }); test('Works with the correct action group spelling', () => { - const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const migration800 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '8.0.0' + ]; const actions = [ { @@ -2346,6 +2437,72 @@ describe('successful migrations', () => { }); }); +describe('search source migration', () => { + it('should apply migration within es query alert rule', () => { + const esQueryRuleSavedObject = { + attributes: { + params: { + searchConfiguration: { + some: 'prop', + migrated: false, + }, + }, + }, + } as SavedObjectUnsanitizedDoc; + + const versionToTest = '9.1.3'; + const migrations = getMigrations( + encryptedSavedObjectsSetup, + { + [versionToTest]: (state) => ({ ...state, migrated: true }), + }, + isPreconfigured + ); + + expect( + migrations[versionToTest](esQueryRuleSavedObject, {} as SavedObjectMigrationContext) + ).toEqual({ + attributes: { + params: { + searchConfiguration: { + some: 'prop', + migrated: true, + }, + }, + }, + }); + }); + + it('should not apply migration within es query alert rule when searchConfiguration not an object', () => { + const esQueryRuleSavedObject = { + attributes: { + params: { + searchConfiguration: 5, + }, + }, + } as SavedObjectUnsanitizedDoc; + + const versionToTest = '9.1.4'; + const migrations = getMigrations( + encryptedSavedObjectsSetup, + { + [versionToTest]: (state) => ({ ...state, migrated: true }), + }, + isPreconfigured + ); + + expect( + migrations[versionToTest](esQueryRuleSavedObject, {} as SavedObjectMigrationContext) + ).toEqual({ + attributes: { + params: { + searchConfiguration: 5, + }, + }, + }); + }); +}); + describe('handles errors during migrations', () => { beforeEach(() => { jest.resetAllMocks(); @@ -2355,7 +2512,7 @@ describe('handles errors during migrations', () => { }); describe('7.10.0 throws if migration fails', () => { test('should show the proper exception', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.10.0']; const alert = getMockData({ consumer: 'alerting', }); @@ -2380,7 +2537,7 @@ describe('handles errors during migrations', () => { describe('7.11.0 throws if migration fails', () => { test('should show the proper exception', () => { - const migration711 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.0']; + const migration711 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)['7.11.0']; const alert = getMockData({ consumer: 'alerting', }); @@ -2405,7 +2562,9 @@ describe('handles errors during migrations', () => { describe('7.11.2 throws if migration fails', () => { test('should show the proper exception', () => { - const migration7112 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.11.2']; + const migration7112 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.11.2' + ]; const alert = getMockData({ consumer: 'alerting', }); @@ -2430,7 +2589,9 @@ describe('handles errors during migrations', () => { describe('7.13.0 throws if migration fails', () => { test('should show the proper exception', () => { - const migration7130 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.13.0']; + const migration7130 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.13.0' + ]; const alert = getMockData({ consumer: 'alerting', }); @@ -2455,7 +2616,9 @@ describe('handles errors during migrations', () => { describe('7.16.0 throws if migration fails', () => { test('should show the proper exception', () => { - const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const migration7160 = getMigrations(encryptedSavedObjectsSetup, {}, isPreconfigured)[ + '7.16.0' + ]; const rule = getMockData(); expect(() => { migration7160(rule, migrationContext); @@ -2475,6 +2638,53 @@ describe('handles errors during migrations', () => { ); }); }); + + describe('8.3.0 throws if migration fails', () => { + test('should show the proper exception on search source migration', () => { + encryptedSavedObjectsSetup.createMigration.mockImplementation(({ migration }) => migration); + const mockRule = getMockData(); + const rule = { + ...mockRule, + attributes: { + ...mockRule.attributes, + params: { + searchConfiguration: { + some: 'prop', + migrated: false, + }, + }, + }, + }; + + const versionToTest = '8.3.0'; + const migration830 = getMigrations( + encryptedSavedObjectsSetup, + { + [versionToTest]: () => { + throw new Error(`Can't migrate search source!`); + }, + }, + isPreconfigured + )[versionToTest]; + + expect(() => { + migration830(rule, migrationContext); + }).toThrowError(`Can't migrate search source!`); + expect(migrationContext.log.error).toHaveBeenCalledWith( + `encryptedSavedObject ${versionToTest} migration failed for alert ${rule.id} with error: Can't migrate search source!`, + { + migrations: { + alertDocument: { + ...rule, + attributes: { + ...rule.attributes, + }, + }, + }, + } + ); + }); + }); }); function getUpdatedAt(): string { diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index 69d88e196dcfd..b3f8d873d8ef0 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -7,6 +7,7 @@ import { isRuleType, ruleTypeMappings } from '@kbn/securitysolution-rules'; import { isString } from 'lodash/fp'; +import { gte } from 'semver'; import { LogMeta, SavedObjectMigrationMap, @@ -19,12 +20,16 @@ import { } from '@kbn/core/server'; import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server'; import type { IsMigrationNeededPredicate } from '@kbn/encrypted-saved-objects-plugin/server'; -import { RawRule, RawRuleAction, RawRuleExecutionStatus } from '../types'; +import { MigrateFunctionsObject, MigrateFunction } from '@kbn/kibana-utils-plugin/common'; +import { mergeSavedObjectMigrationMaps } from '@kbn/core/server'; +import { isSerializedSearchSource, SerializedSearchSourceFields } from '@kbn/data-plugin/common'; import { extractRefsFromGeoContainmentAlert } from './geo_containment/migrations'; +import { RawRule, RawRuleAction, RawRuleExecutionStatus } from '../types'; import { getMappedParams } from '../rules_client/lib/mapped_params_utils'; const SIEM_APP_ID = 'securitySolution'; const SIEM_SERVER_APP_ID = 'siem'; +const MINIMUM_SS_MIGRATION_VERSION = '8.3.0'; export const LEGACY_LAST_MODIFIED_VERSION = 'pre-7.10.0'; export const FILEBEAT_7X_INDICATOR_PATH = 'threatintel.indicator'; @@ -59,6 +64,9 @@ export const isAnyActionSupportIncidents = (doc: SavedObjectUnsanitizedDoc): boolean => doc.attributes.alertTypeId === 'siem.signals'; +export const isEsQueryRuleType = (doc: SavedObjectUnsanitizedDoc) => + doc.attributes.alertTypeId === '.es-query'; + export const isDetectionEngineAADRuleType = (doc: SavedObjectUnsanitizedDoc): boolean => (Object.values(ruleTypeMappings) as string[]).includes(doc.attributes.alertTypeId); @@ -75,6 +83,7 @@ export const isSecuritySolutionLegacyNotification = ( export function getMigrations( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, + searchSourceMigrations: MigrateFunctionsObject, isPreconfigured: (connectorId: string) => boolean ): SavedObjectMigrationMap { const migrationWhenRBACWasIntroduced = createEsoMigration( @@ -155,22 +164,25 @@ export function getMigrations( const migrationRules830 = createEsoMigration( encryptedSavedObjects, (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => true, - pipeMigrations(removeInternalTags) + pipeMigrations(addSearchType, removeInternalTags) ); - return { - '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), - '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'), - '7.11.2': executeMigrationWithErrorHandling(migrationActions7112, '7.11.2'), - '7.13.0': executeMigrationWithErrorHandling(migrationSecurityRules713, '7.13.0'), - '7.14.1': executeMigrationWithErrorHandling(migrationSecurityRules714, '7.14.1'), - '7.15.0': executeMigrationWithErrorHandling(migrationSecurityRules715, '7.15.0'), - '7.16.0': executeMigrationWithErrorHandling(migrateRules716, '7.16.0'), - '8.0.0': executeMigrationWithErrorHandling(migrationRules800, '8.0.0'), - '8.0.1': executeMigrationWithErrorHandling(migrationRules801, '8.0.1'), - '8.2.0': executeMigrationWithErrorHandling(migrationRules820, '8.2.0'), - '8.3.0': executeMigrationWithErrorHandling(migrationRules830, '8.3.0'), - }; + return mergeSavedObjectMigrationMaps( + { + '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), + '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'), + '7.11.2': executeMigrationWithErrorHandling(migrationActions7112, '7.11.2'), + '7.13.0': executeMigrationWithErrorHandling(migrationSecurityRules713, '7.13.0'), + '7.14.1': executeMigrationWithErrorHandling(migrationSecurityRules714, '7.14.1'), + '7.15.0': executeMigrationWithErrorHandling(migrationSecurityRules715, '7.15.0'), + '7.16.0': executeMigrationWithErrorHandling(migrateRules716, '7.16.0'), + '8.0.0': executeMigrationWithErrorHandling(migrationRules800, '8.0.0'), + '8.0.1': executeMigrationWithErrorHandling(migrationRules801, '8.0.1'), + '8.2.0': executeMigrationWithErrorHandling(migrationRules820, '8.2.0'), + '8.3.0': executeMigrationWithErrorHandling(migrationRules830, '8.3.0'), + }, + getSearchSourceMigrations(encryptedSavedObjects, searchSourceMigrations) + ); } function executeMigrationWithErrorHandling( @@ -697,6 +709,23 @@ function addSecuritySolutionAADRuleTypes( : doc; } +function addSearchType(doc: SavedObjectUnsanitizedDoc) { + const searchType = doc.attributes.params.searchType; + + return isEsQueryRuleType(doc) && !searchType + ? { + ...doc, + attributes: { + ...doc.attributes, + params: { + ...doc.attributes.params, + searchType: 'esQuery', + }, + }, + } + : doc; +} + function addSecuritySolutionAADRuleTypeTags( doc: SavedObjectUnsanitizedDoc ): SavedObjectUnsanitizedDoc { @@ -902,3 +931,56 @@ function pipeMigrations(...migrations: AlertMigration[]): AlertMigration { return (doc: SavedObjectUnsanitizedDoc) => migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); } + +function mapSearchSourceMigrationFunc( + migrateSerializedSearchSourceFields: MigrateFunction +): MigrateFunction { + return (doc) => { + const _doc = doc as { attributes: RawRule }; + + const serializedSearchSource = _doc.attributes.params.searchConfiguration; + + if (isSerializedSearchSource(serializedSearchSource)) { + return { + ..._doc, + attributes: { + ..._doc.attributes, + params: { + ..._doc.attributes.params, + searchConfiguration: migrateSerializedSearchSourceFields(serializedSearchSource), + }, + }, + }; + } + return _doc; + }; +} + +/** + * This creates a migration map that applies search source migrations to legacy es query rules. + * It doesn't modify existing migrations. The following migrations will occur at minimum version of 8.3+. + */ +function getSearchSourceMigrations( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, + searchSourceMigrations: MigrateFunctionsObject +) { + const filteredMigrations: SavedObjectMigrationMap = {}; + for (const versionKey in searchSourceMigrations) { + if (gte(versionKey, MINIMUM_SS_MIGRATION_VERSION)) { + const migrateSearchSource = mapSearchSourceMigrationFunc( + searchSourceMigrations[versionKey] + ) as unknown as AlertMigration; + + filteredMigrations[versionKey] = executeMigrationWithErrorHandling( + createEsoMigration( + encryptedSavedObjects, + (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => + isEsQueryRuleType(doc), + pipeMigrations(migrateSearchSource) + ), + versionKey + ); + } + } + return filteredMigrations; +} diff --git a/x-pack/plugins/apm/dev_docs/testing.md b/x-pack/plugins/apm/dev_docs/testing.md index 6c35979add784..e1819875f58d8 100644 --- a/x-pack/plugins/apm/dev_docs/testing.md +++ b/x-pack/plugins/apm/dev_docs/testing.md @@ -96,13 +96,13 @@ TODO: We could try moving this tests to the new e2e tests located at `x-pack/plu **Start server** ``` -node scripts/functional_tests_server --config x-pack/test/functional/config.js +node scripts/functional_tests_server --config x-pack/test/functional/config.base.js ``` **Run tests** ``` -node scripts/functional_test_runner --config x-pack/test/functional/config.js --grep='APM specs' +node scripts/functional_test_runner --config x-pack/test/functional/config.base.js --grep='APM specs' ``` APM tests are located in `x-pack/test/functional/apps/apm`. diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts index edb1b8a82f6dd..caeac36b7cba8 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts @@ -220,14 +220,18 @@ describe('Service Overview', () => { 'suggestionsRequest' ); - cy.get('[data-test-subj="environmentFilter"]').type('pro').click(); + cy.get('[data-test-subj="environmentFilter"]').type('production'); cy.expectAPIsToHaveBeenCalledWith({ apisIntercepted: ['@suggestionsRequest'], - value: 'fieldValue=pro', + value: 'fieldValue=production', }); - cy.contains('button', 'production').click(); + cy.get( + '[data-test-subj="comboBoxOptionsList environmentFilter-optionsList"]' + ) + .contains('production') + .click({ force: true }); cy.expectAPIsToHaveBeenCalledWith({ apisIntercepted: aliasNames, diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts b/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts index 383a6fa68cf8a..2d9b530ed76a9 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts @@ -19,7 +19,7 @@ const NODE_TLS_REJECT_UNAUTHORIZED = '1'; export const esArchiverLoad = (archiveName: string) => { const archivePath = path.join(ES_ARCHIVE_DIR, archiveName); execSync( - `node ../../../../scripts/es_archiver load "${archivePath}" --config ../../../test/functional/config.js`, + `node ../../../../scripts/es_archiver load "${archivePath}" --config ../../../test/functional/config.base.js`, { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; @@ -27,14 +27,14 @@ export const esArchiverLoad = (archiveName: string) => { export const esArchiverUnload = (archiveName: string) => { const archivePath = path.join(ES_ARCHIVE_DIR, archiveName); execSync( - `node ../../../../scripts/es_archiver unload "${archivePath}" --config ../../../test/functional/config.js`, + `node ../../../../scripts/es_archiver unload "${archivePath}" --config ../../../test/functional/config.base.js`, { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; export const esArchiverResetKibana = () => { execSync( - `node ../../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.js`, + `node ../../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.base.js`, { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; diff --git a/x-pack/plugins/apm/ftr_e2e/ftr_config.ts b/x-pack/plugins/apm/ftr_e2e/ftr_config.ts index ec2e8d05a97dd..dd2166508d134 100644 --- a/x-pack/plugins/apm/ftr_e2e/ftr_config.ts +++ b/x-pack/plugins/apm/ftr_e2e/ftr_config.ts @@ -13,7 +13,7 @@ async function config({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../../test/common/config.js') ); const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); return { diff --git a/x-pack/plugins/apm/public/components/app/settings/custom_link/empty_prompt.tsx b/x-pack/plugins/apm/public/components/app/settings/custom_link/empty_prompt.tsx index dd9cd760d70cf..fd7a3d2587190 100644 --- a/x-pack/plugins/apm/public/components/app/settings/custom_link/empty_prompt.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/custom_link/empty_prompt.tsx @@ -5,16 +5,19 @@ * 2.0. */ -import { EuiEmptyPrompt } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiLink, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; import { CreateCustomLinkButton } from './create_custom_link_button'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; export function EmptyPrompt({ onCreateCustomLinkClick, }: { onCreateCustomLinkClick: () => void; }) { + const { docLinks } = useApmPluginContext().core; return ( -

- {i18n.translate('xpack.apm.settings.customLink.emptyPromptText', { - defaultMessage: - "Let's change that! You can add custom links to the Actions context menu by the transaction details for each service. Create a helpful link to your company's support portal or open a new bug report. Learn more about it in our docs.", - })} -

+ + + {i18n.translate( + 'xpack.apm.settings.customLink.emptyPromptText.customLinkDocLinkText', + { defaultMessage: 'docs' } + )} + + ), + }} + /> + } actions={} diff --git a/x-pack/plugins/apm/public/components/app/settings/custom_link/index.test.tsx b/x-pack/plugins/apm/public/components/app/settings/custom_link/index.test.tsx index e9979746426a3..40f8f5ad1db25 100644 --- a/x-pack/plugins/apm/public/components/app/settings/custom_link/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/custom_link/index.test.tsx @@ -23,6 +23,7 @@ import { expectTextsNotInDocument, } from '../../../../utils/test_helpers'; import * as saveCustomLink from './create_edit_custom_link_flyout/save_custom_link'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; const data = { customLinks: [ @@ -73,9 +74,11 @@ describe('CustomLink', () => { it('shows when no link is available', () => { const component = render( - - - + + + + + ); expectTextsInDocument(component, ['No links found.']); @@ -360,9 +363,11 @@ describe('CustomLink', () => { }); const component = render( - - - + + + + + ); expectTextsNotInDocument(component, ['Start free 30-day trial']); @@ -375,9 +380,11 @@ describe('CustomLink', () => { const { getByTestId } = render( - - - + + + + + ); const createButton = getByTestId('createButton') as HTMLButtonElement; @@ -389,9 +396,11 @@ describe('CustomLink', () => { const { queryAllByText } = render( - - - + + + + + ); diff --git a/x-pack/plugins/apm/public/components/app/settings/schema/schema_overview.tsx b/x-pack/plugins/apm/public/components/app/settings/schema/schema_overview.tsx index 1e31eb8ccbe67..1f17b9f63c0a0 100644 --- a/x-pack/plugins/apm/public/components/app/settings/schema/schema_overview.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/schema/schema_overview.tsx @@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiLink, EuiLoadingSpinner, EuiSpacer, EuiText, @@ -21,11 +22,11 @@ import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; import semverLt from 'semver/functions/lt'; import { PackagePolicy } from '@kbn/fleet-plugin/common/types'; -import { ElasticDocsLink } from '../../../shared/links/elastic_docs_link'; import rocketLaunchGraphic from './blog_rocket_720x420.png'; import { MigrationInProgressPanel } from './migration_in_progress_panel'; import { UpgradeAvailableCard } from './migrated/upgrade_available_card'; import { SuccessfulMigrationCard } from './migrated/successful_migration_card'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; interface Props { onSwitch: () => void; @@ -184,6 +185,7 @@ export function SchemaOverview({ } export function SchemaOverviewHeading() { + const { docLinks } = useApmPluginContext().core; return ( <> @@ -208,16 +210,12 @@ export function SchemaOverviewHeading() { ), elasticAgentDocLink: ( - + {i18n.translate( 'xpack.apm.settings.schema.descriptionText.elasticAgentDocLinkText', { defaultMessage: 'Elastic Agent' } )} - + ), }} /> diff --git a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx index 0ff0b18818301..ad6947c736fe2 100644 --- a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx +++ b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx @@ -21,13 +21,9 @@ import { ServiceGroupsList } from '../app/service_groups'; import { ServiceGroupsRedirect } from './service_groups_redirect'; import { offsetRt } from '../../../common/offset_rt'; -const ServiceGroupsBreadcrumnbLabel = i18n.translate( - 'xpack.apm.views.serviceGroups.breadcrumbLabel', - { defaultMessage: 'Services' } -); const ServiceGroupsTitle = i18n.translate( 'xpack.apm.views.serviceGroups.title', - { defaultMessage: 'Service groups' } + { defaultMessage: 'Services' } ); /** @@ -77,10 +73,7 @@ const apmRoutes = { // this route fails on navigation unless it's defined before home '/service-groups': { element: ( - + diff --git a/x-pack/plugins/apm/public/components/shared/backend_link.tsx b/x-pack/plugins/apm/public/components/shared/backend_link.tsx index 6dde4e269ec54..ac93228c76bcd 100644 --- a/x-pack/plugins/apm/public/components/shared/backend_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/backend_link.tsx @@ -42,7 +42,9 @@ export function BackendLink({ - {query.backendName} + + {query.backendName} + ); diff --git a/x-pack/plugins/apm/public/components/shared/service_link.tsx b/x-pack/plugins/apm/public/components/shared/service_link.tsx index dc4f56ac57053..00a360f3fdebb 100644 --- a/x-pack/plugins/apm/public/components/shared/service_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/service_link.tsx @@ -42,7 +42,9 @@ export function ServiceLink({ - {serviceName} + + {serviceName} + ); diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 627196a9a4bdc..2ee87571cb719 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -93,6 +93,14 @@ export interface ApmPluginStartDeps { const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', { defaultMessage: 'Services', }); + +const allServicesTitle = i18n.translate( + 'xpack.apm.navigation.allServicesTitle', + { + defaultMessage: 'All services', + } +); + const tracesTitle = i18n.translate('xpack.apm.navigation.tracesTitle', { defaultMessage: 'Traces', }); @@ -270,12 +278,16 @@ export class ApmPlugin implements Plugin { ? [ { id: 'service-groups-list', - title: 'Service groups', + title: servicesTitle, path: '/service-groups', }, ] : []), - { id: 'services', title: servicesTitle, path: '/services' }, + { + id: 'services', + title: serviceGroupsEnabled ? allServicesTitle : servicesTitle, + path: '/services', + }, { id: 'traces', title: tracesTitle, path: '/traces' }, { id: 'service-map', title: serviceMapTitle, path: '/service-map' }, { id: 'backends', title: dependenciesTitle, path: '/backends' }, diff --git a/x-pack/plugins/apm/public/services/rest/create_call_apm_api.ts b/x-pack/plugins/apm/public/services/rest/create_call_apm_api.ts index e9450ec67cc88..4d6ca03ba6a29 100644 --- a/x-pack/plugins/apm/public/services/rest/create_call_apm_api.ts +++ b/x-pack/plugins/apm/public/services/rest/create_call_apm_api.ts @@ -8,16 +8,11 @@ import { CoreSetup, CoreStart } from '@kbn/core/public'; import type { ClientRequestParamsOf, - formatRequest as formatRequestType, ReturnOf, RouteRepositoryClient, ServerRouteRepository, } from '@kbn/server-route-repository'; -// @ts-expect-error cannot find module or correspondent type declarations -// The code and types are at separated folders on @kbn/server-route-repository -// so in order to do targeted imports they must me imported separately, and -// an error is expected here -import { formatRequest } from '@kbn/server-route-repository/target_node/format_request'; +import { formatRequest } from '@kbn/server-route-repository'; import { InspectResponse } from '@kbn/observability-plugin/typings/common'; import { FetchOptions } from '../../../common/fetch_options'; import { CallApi, callApi } from './call_api'; @@ -73,10 +68,7 @@ export function createCallApmApi(core: CoreStart | CoreSetup) { params?: Partial>; }; - const { method, pathname } = formatRequest( - endpoint, - params?.path - ) as ReturnType; + const { method, pathname } = formatRequest(endpoint, params?.path); return callApi(core, { ...options, diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 3f42e5b5c875c..b3dbe4801f544 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -39,6 +39,20 @@ export const SettingsRt = rt.type({ syncAlerts: rt.boolean, }); +export enum CaseSeverity { + LOW = 'low', + MEDIUM = 'medium', + HIGH = 'high', + CRITICAL = 'critical', +} + +export const CaseSeverityRt = rt.union([ + rt.literal(CaseSeverity.LOW), + rt.literal(CaseSeverity.MEDIUM), + rt.literal(CaseSeverity.HIGH), + rt.literal(CaseSeverity.CRITICAL), +]); + const CaseBasicRt = rt.type({ /** * The description of the case @@ -68,6 +82,10 @@ const CaseBasicRt = rt.type({ * The plugin owner of the case */ owner: rt.string, + /** + * The severity of the case + */ + severity: CaseSeverityRt, }); /** @@ -106,33 +124,42 @@ export const CaseAttributesRt = rt.intersection([ }), ]); -export const CasePostRequestRt = rt.type({ - /** - * Description of the case - */ - description: rt.string, - /** - * Identifiers for the case. - */ - tags: rt.array(rt.string), - /** - * Title of the case - */ - title: rt.string, - /** - * The external configuration for the case - */ - connector: CaseConnectorRt, - /** - * Sync settings for alerts - */ - settings: SettingsRt, - /** - * The owner here must match the string used when a plugin registers a feature with access to the cases plugin. The user - * creating this case must also be granted access to that plugin's feature. - */ - owner: rt.string, -}); +export const CasePostRequestRt = rt.intersection([ + rt.type({ + /** + * Description of the case + */ + description: rt.string, + /** + * Identifiers for the case. + */ + tags: rt.array(rt.string), + /** + * Title of the case + */ + title: rt.string, + /** + * The external configuration for the case + */ + connector: CaseConnectorRt, + /** + * Sync settings for alerts + */ + settings: SettingsRt, + /** + * The owner here must match the string used when a plugin registers a feature with access to the cases plugin. The user + * creating this case must also be granted access to that plugin's feature. + */ + owner: rt.string, + }), + rt.partial({ + /** + * The severity of the case. The severity is + * default it to "low" if not provided. + */ + severity: CaseSeverityRt, + }), +]); export const CasesFindRequestRt = rt.partial({ /** diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/common.ts b/x-pack/plugins/cases/common/api/cases/user_actions/common.ts index a6d12d135c142..5665ab524071a 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions/common.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions/common.ts @@ -17,6 +17,7 @@ export const ActionTypes = { title: 'title', status: 'status', settings: 'settings', + severity: 'severity', create_case: 'create_case', delete_case: 'delete_case', } as const; diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/create_case.ts b/x-pack/plugins/cases/common/api/cases/user_actions/create_case.ts index c491cc519132f..53d2320b5afd4 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions/create_case.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions/create_case.ts @@ -23,6 +23,7 @@ export const CommonFieldsRt = rt.type({ const CommonPayloadAttributesRt = rt.type({ description: DescriptionUserActionPayloadRt.props.description, status: rt.string, + severity: rt.string, tags: TagsUserActionPayloadRt.props.tags, title: TitleUserActionPayloadRt.props.title, settings: SettingsUserActionPayloadRt.props.settings, diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/index.ts b/x-pack/plugins/cases/common/api/cases/user_actions/index.ts index 3f974d89fc79a..d19ee5fcbe9f8 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions/index.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions/index.ts @@ -23,6 +23,7 @@ import { TitleUserActionRt } from './title'; import { SettingsUserActionRt } from './settings'; import { StatusUserActionRt } from './status'; import { DeleteCaseUserActionRt } from './delete_case'; +import { SeverityUserActionRt } from './severity'; export * from './common'; export * from './comment'; @@ -43,6 +44,7 @@ const CommonUserActionsRt = rt.union([ TitleUserActionRt, SettingsUserActionRt, StatusUserActionRt, + SeverityUserActionRt, ]); export const UserActionsRt = rt.union([ diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/severity.ts b/x-pack/plugins/cases/common/api/cases/user_actions/severity.ts new file mode 100644 index 0000000000000..2db5a0880dc09 --- /dev/null +++ b/x-pack/plugins/cases/common/api/cases/user_actions/severity.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 * as rt from 'io-ts'; +import { CaseSeverityRt } from '../case'; +import { ActionTypes, UserActionWithAttributes } from './common'; + +export const SeverityUserActionPayloadRt = rt.type({ severity: CaseSeverityRt }); + +export const SeverityUserActionRt = rt.type({ + type: rt.literal(ActionTypes.severity), + payload: SeverityUserActionPayloadRt, +}); + +export type SeverityUserAction = UserActionWithAttributes>; diff --git a/x-pack/plugins/cases/common/api/metrics/case.ts b/x-pack/plugins/cases/common/api/metrics/case.ts index 503d602cf6729..2c3a65764769f 100644 --- a/x-pack/plugins/cases/common/api/metrics/case.ts +++ b/x-pack/plugins/cases/common/api/metrics/case.ts @@ -184,6 +184,9 @@ export const SingleCaseMetricsResponseRt = rt.partial( export const CasesMetricsResponseRt = rt.partial( rt.type({ + /** + * The average resolve time of all cases in seconds + */ mttr: rt.number, }).props ); diff --git a/x-pack/plugins/cases/common/api/runtime_types.ts b/x-pack/plugins/cases/common/api/runtime_types.ts index c807d4b31b751..0a31479b29da8 100644 --- a/x-pack/plugins/cases/common/api/runtime_types.ts +++ b/x-pack/plugins/cases/common/api/runtime_types.ts @@ -9,42 +9,18 @@ import { either, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; -import { isObject } from 'lodash/fp'; +import { formatErrors } from '@kbn/securitysolution-io-ts-utils'; type ErrorFactory = (message: string) => Error; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts - * Bug fix for the TODO is in the format_errors package - */ -export const formatErrors = (errors: rt.Errors): string[] => { - const err = errors.map((error) => { - if (error.message != null) { - return error.message; - } else { - const keyContext = error.context - .filter( - (entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== '' - ) - .map((entry) => entry.key) - .join(','); - - const nameContext = error.context.find((entry) => { - // TODO: Put in fix for optional chaining https://github.com/cypress-io/cypress/issues/9298 - if (entry.type && entry.type.name) { - return entry.type.name.length > 0; - } - return false; - }); - const suppliedValue = - keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : ''; - const value = isObject(error.value) ? JSON.stringify(error.value) : error.value; - return `Invalid value "${value}" supplied to "${suppliedValue}"`; - } - }); - - return [...new Set(err)]; -}; +export type GenericIntersectionC = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any, any, any]>; export const createPlainError = (message: string) => new Error(message); @@ -57,6 +33,40 @@ export const decodeOrThrow = (inputValue: I) => pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity)); +const getProps = ( + codec: + | rt.HasProps + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.RecordC + | GenericIntersectionC +): rt.Props | null => { + if (codec == null) { + return null; + } + switch (codec._tag) { + case 'DictionaryType': + if (codec.codomain.props != null) { + return codec.codomain.props; + } + const dTypes: rt.HasProps[] = codec.codomain.types; + return dTypes.reduce((props, type) => Object.assign(props, getProps(type)), {}); + case 'RefinementType': + case 'ReadonlyType': + return getProps(codec.type); + case 'InterfaceType': + case 'StrictType': + case 'PartialType': + return codec.props; + case 'IntersectionType': + const iTypes = codec.types as rt.HasProps[]; + return iTypes.reduce((props, type) => { + return Object.assign(props, getProps(type) as rt.Props); + }, {} as rt.Props) as rt.Props; + default: + return null; + } +}; + const getExcessProps = (props: rt.Props, r: Record): string[] => { const ex: string[] = []; for (const k of Object.keys(r)) { @@ -67,15 +77,21 @@ const getExcessProps = (props: rt.Props, r: Record): string[] = return ex; }; -export function excess | rt.PartialType>( - codec: C -): C { +export function excess< + C extends rt.InterfaceType | GenericIntersectionC | rt.PartialType +>(codec: C): C { + const codecProps = getProps(codec); + const r = new rt.InterfaceType( codec.name, codec.is, (i, c) => either.chain(rt.UnknownRecord.validate(i, c), (s: Record) => { - const ex = getExcessProps(codec.props, s); + if (codecProps == null) { + return rt.failure(i, c, 'unknown codec'); + } + + const ex = getExcessProps(codecProps, s); return ex.length > 0 ? rt.failure( i, @@ -87,7 +103,7 @@ export function excess | rt.PartialType; export type CaseUserActions = SnakeToCamelCase; export type CaseExternalService = SnakeToCamelCase; export type Case = Omit, 'comments'> & { comments: Comment[] }; +export type Cases = Omit, 'cases'> & { cases: Case[] }; +export type CasesStatus = SnakeToCamelCase; +export type CasesMetrics = SnakeToCamelCase; export interface ResolvedCase { case: Case; @@ -84,19 +90,6 @@ export interface FilterOptions { owner: string[]; } -export interface CasesStatus { - countClosedCases: number | null; - countOpenCases: number | null; - countInProgressCases: number | null; -} - -export interface AllCases extends CasesStatus { - cases: Case[]; - page: number; - perPage: number; - total: number; -} - export type SingleCaseMetrics = SingleCaseMetricsResponse; export type SingleCaseMetricsFeature = | 'alerts.count' @@ -151,7 +144,7 @@ export interface FieldMappings { export type UpdateKey = keyof Pick< CasePatchRequest, - 'connector' | 'description' | 'status' | 'tags' | 'title' | 'settings' + 'connector' | 'description' | 'status' | 'tags' | 'title' | 'settings' | 'severity' >; export interface UpdateByKey { diff --git a/x-pack/plugins/cases/docs/openapi/README.md b/x-pack/plugins/cases/docs/openapi/README.md new file mode 100644 index 0000000000000..1ff3e24c2e91f --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/README.md @@ -0,0 +1,29 @@ +# OpenAPI (Experimental) + +The current self-contained spec file is [as JSON](https://raw.githubusercontent.com/elastic/kibana/master/x-pack/plugins/cases/common/openapi/bundled.json) or [as YAML](https://raw.githubusercontent.com/elastic/kibana/master/x-pack/plugins/cases/common/openapi/bundled.yaml) and can be used for online tools like those found at https://openapi.tools/. +This spec is experimental and may be incomplete or change later. + +A guide about the openApi specification can be found at [https://swagger.io/docs/specification/about/](https://swagger.io/docs/specification/about/). + +## The `openapi` folder + +* `entrypoint.yaml` is the overview file which pulls together all the paths and components. +* [Paths](paths/README.md): this defines each endpoint. A path can have one operation per http method. +* [Components](components/README.md): Reusable components + +## Tools + +It is possible to validate the docs before bundling them with the following +command in the `x-pack/plugins/cases/docs/openapi/` folder: + + ``` + npx swagger-cli validate entrypoint.yaml + ``` + +Then you can generate the `bundled` files by running the following commands: + + ``` + npx @redocly/openapi-cli bundle --ext yaml --output bundled.yaml entrypoint.yaml + npx @redocly/openapi-cli bundle --ext json --output bundled.json entrypoint.yaml + ``` + diff --git a/x-pack/plugins/cases/docs/openapi/bundled.json b/x-pack/plugins/cases/docs/openapi/bundled.json new file mode 100644 index 0000000000000..31feae3331b04 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/bundled.json @@ -0,0 +1,2122 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Cases", + "description": "OpenAPI schema for Cases endpoints", + "version": "0.1", + "contact": { + "name": "Cases Team" + }, + "license": { + "name": "Elastic License 2.0", + "url": "https://www.elastic.co/licensing/elastic-license" + } + }, + "tags": [ + { + "name": "cases", + "description": "Case APIs enable you to open and track issues." + }, + { + "name": "kibana", + "description": "Kibana APIs enable you to interact with Kibana features." + } + ], + "servers": [ + { + "url": "http://localhost:5601", + "description": "local" + } + ], + "paths": { + "/api/cases": { + "post": { + "description": "Creates a case. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're creating.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connector": { + "description": "An object that contains the connector configuration.", + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "description": { + "description": "The description for the case.", + "type": "string" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "description": "An object that contains the case settings.", + "type": "object", + "properties": { + "syncAlerts": { + "description": "Turns alert syncing on or off.", + "type": "boolean" + } + } + }, + "tags": { + "description": "The words and phrases that help categorize cases. It can be an empty array.", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "description": "A title for the case.", + "type": "string" + } + }, + "required": [ + "connector", + "description", + "owner", + "settings", + "tags", + "title" + ] + }, + "examples": { + "createCaseRequest": { + "$ref": "#/components/examples/create_case_request" + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "closed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "comments": { + "type": "array", + "items": { + "type": "string" + }, + "example": [] + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-05-13T09:16:17.416Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "ahunley@imf.usa.gov" + }, + "full_name": { + "type": "string", + "example": "Alan Hunley" + }, + "username": { + "type": "string", + "example": "ahunley" + } + } + }, + "description": { + "type": "string", + "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" + }, + "external_service": { + "type": "object", + "properties": { + "connector_id": { + "type": "string" + }, + "connector_name": { + "type": "string" + }, + "external_id": { + "type": "string" + }, + "external_title": { + "type": "string" + }, + "external_url": { + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time" + }, + "pushed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + } + } + }, + "id": { + "type": "string", + "example": "66b9aa00-94fa-11ea-9f74-e7e108796192" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "type": "object", + "properties": { + "syncAlerts": { + "type": "boolean", + "example": true + } + } + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "phishing", + "social engineering", + "bubblegum" + ] + }, + "title": { + "type": "string", + "example": "This case will self-destruct in 5 seconds" + }, + "totalAlerts": { + "type": "integer", + "example": 0 + }, + "totalComment": { + "type": "integer", + "example": 0 + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "version": { + "type": "string", + "example": "WzUzMiwxXQ==" + } + } + }, + "examples": { + "createCaseResponse": { + "$ref": "#/components/examples/create_case_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "delete": { + "description": "Deletes one or more cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're deleting.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "name": "ids", + "description": "The cases that you want to removed. To retrieve case IDs, use the find cases API. All non-ASCII characters must be URL encoded.", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "example": "d4e7abb0-b462-11ec-9a8d-698504725a43" + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "patch": { + "description": "Updates one or more cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "cases": { + "type": "array", + "items": { + "type": "object", + "properties": { + "connector": { + "description": "An object that contains the connector configuration.", + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "description": { + "description": "The description for the case.", + "type": "string" + }, + "id": { + "description": "The identifier for the case.", + "type": "string" + }, + "settings": { + "description": "An object that contains the case settings.", + "type": "object", + "properties": { + "syncAlerts": { + "description": "Turns alert syncing on or off.", + "type": "boolean" + } + } + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "description": "The words and phrases that help categorize cases.", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "description": "A title for the case.", + "type": "string" + }, + "version": { + "description": "The current version of the case.", + "type": "string" + } + }, + "required": [ + "id", + "version" + ] + } + } + } + }, + "examples": { + "updateCaseRequest": { + "$ref": "#/components/examples/update_case_request" + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "closed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "comments": { + "type": "array", + "items": { + "type": "string" + }, + "example": [] + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-05-13T09:16:17.416Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "ahunley@imf.usa.gov" + }, + "full_name": { + "type": "string", + "example": "Alan Hunley" + }, + "username": { + "type": "string", + "example": "ahunley" + } + } + }, + "description": { + "type": "string", + "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" + }, + "external_service": { + "type": "object", + "properties": { + "connector_id": { + "type": "string" + }, + "connector_name": { + "type": "string" + }, + "external_id": { + "type": "string" + }, + "external_title": { + "type": "string" + }, + "external_url": { + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time" + }, + "pushed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + } + } + }, + "id": { + "type": "string", + "example": "66b9aa00-94fa-11ea-9f74-e7e108796192" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "type": "object", + "properties": { + "syncAlerts": { + "type": "boolean", + "example": true + } + } + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "phishing", + "social engineering", + "bubblegum" + ] + }, + "title": { + "type": "string", + "example": "This case will self-destruct in 5 seconds" + }, + "totalAlerts": { + "type": "integer", + "example": 0 + }, + "totalComment": { + "type": "integer", + "example": 0 + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "version": { + "type": "string", + "example": "WzUzMiwxXQ==" + } + } + }, + "examples": { + "updateCaseResponse": { + "$ref": "#/components/examples/update_case_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/cases": { + "post": { + "description": "Creates a case. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're creating.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connector": { + "description": "An object that contains the connector configuration.", + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "description": { + "description": "The description for the case.", + "type": "string" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "description": "An object that contains the case settings.", + "type": "object", + "properties": { + "syncAlerts": { + "description": "Turns alert syncing on or off.", + "type": "boolean" + } + } + }, + "tags": { + "description": "The words and phrases that help categorize cases. It can be an empty array.", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "description": "A title for the case.", + "type": "string" + } + }, + "required": [ + "connector", + "description", + "owner", + "settings", + "tags", + "title" + ] + }, + "examples": { + "createCaseRequest": { + "$ref": "#/components/examples/create_case_request" + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "closed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "comments": { + "type": "array", + "items": { + "type": "string" + }, + "example": [] + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-05-13T09:16:17.416Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "ahunley@imf.usa.gov" + }, + "full_name": { + "type": "string", + "example": "Alan Hunley" + }, + "username": { + "type": "string", + "example": "ahunley" + } + } + }, + "description": { + "type": "string", + "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" + }, + "external_service": { + "type": "object", + "properties": { + "connector_id": { + "type": "string" + }, + "connector_name": { + "type": "string" + }, + "external_id": { + "type": "string" + }, + "external_title": { + "type": "string" + }, + "external_url": { + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time" + }, + "pushed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + } + } + }, + "id": { + "type": "string", + "example": "66b9aa00-94fa-11ea-9f74-e7e108796192" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "type": "object", + "properties": { + "syncAlerts": { + "type": "boolean", + "example": true + } + } + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "phishing", + "social engineering", + "bubblegum" + ] + }, + "title": { + "type": "string", + "example": "This case will self-destruct in 5 seconds" + }, + "totalAlerts": { + "type": "integer", + "example": 0 + }, + "totalComment": { + "type": "integer", + "example": 0 + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "version": { + "type": "string", + "example": "WzUzMiwxXQ==" + } + } + }, + "examples": { + "createCaseResponse": { + "$ref": "#/components/examples/create_case_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "delete": { + "description": "Deletes one or more cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're deleting.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "name": "ids", + "description": "The cases that you want to removed. All non-ASCII characters must be URL encoded.", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "example": "d4e7abb0-b462-11ec-9a8d-698504725a43" + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "patch": { + "description": "Updates one or more cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "cases": { + "type": "array", + "items": { + "type": "object", + "properties": { + "connector": { + "description": "An object that contains the connector configuration.", + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "description": { + "description": "The description for the case.", + "type": "string" + }, + "id": { + "description": "The identifier for the case.", + "type": "string" + }, + "settings": { + "description": "An object that contains the case settings.", + "type": "object", + "properties": { + "syncAlerts": { + "description": "Turns alert syncing on or off.", + "type": "boolean" + } + } + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "description": "The words and phrases that help categorize cases.", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "description": "A title for the case.", + "type": "string" + }, + "version": { + "description": "The current version of the case.", + "type": "string" + } + }, + "required": [ + "id", + "version" + ] + } + } + } + }, + "examples": { + "updateCaseRequest": { + "$ref": "#/components/examples/update_case_request" + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "closed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "comments": { + "type": "array", + "items": { + "type": "string" + }, + "example": [] + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-05-13T09:16:17.416Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "ahunley@imf.usa.gov" + }, + "full_name": { + "type": "string", + "example": "Alan Hunley" + }, + "username": { + "type": "string", + "example": "ahunley" + } + } + }, + "description": { + "type": "string", + "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" + }, + "external_service": { + "type": "object", + "properties": { + "connector_id": { + "type": "string" + }, + "connector_name": { + "type": "string" + }, + "external_id": { + "type": "string" + }, + "external_title": { + "type": "string" + }, + "external_url": { + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time" + }, + "pushed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + } + } + }, + "id": { + "type": "string", + "example": "66b9aa00-94fa-11ea-9f74-e7e108796192" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "type": "object", + "properties": { + "syncAlerts": { + "type": "boolean", + "example": true + } + } + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "phishing", + "social engineering", + "bubblegum" + ] + }, + "title": { + "type": "string", + "example": "This case will self-destruct in 5 seconds" + }, + "totalAlerts": { + "type": "integer", + "example": 0 + }, + "totalComment": { + "type": "integer", + "example": 0 + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "version": { + "type": "string", + "example": "WzUzMiwxXQ==" + } + } + }, + "examples": { + "updateCaseResponse": { + "$ref": "#/components/examples/update_case_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + } + }, + "components": { + "securitySchemes": { + "basicAuth": { + "type": "http", + "scheme": "basic" + }, + "apiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "ApiKey" + } + }, + "parameters": { + "kbn_xsrf": { + "schema": { + "type": "string" + }, + "in": "header", + "name": "kbn-xsrf", + "required": true + }, + "space_id": { + "in": "path", + "name": "spaceId", + "description": "An identifier for the space.", + "required": true, + "schema": { + "type": "string", + "example": "default" + } + } + }, + "schemas": { + "connector_types": { + "type": "string", + "description": "The type of connector.", + "enum": [ + ".jira", + ".none", + ".resilient", + ".servicenow", + ".servicenow-sir", + ".swimlane" + ] + }, + "owners": { + "type": "string", + "description": "Owner apps", + "enum": [ + "cases", + "observability", + "securitySolution" + ] + }, + "status": { + "type": "string", + "description": "The status of the case.", + "enum": [ + "closed", + "in-progress", + "open" + ] + } + }, + "examples": { + "create_case_request": { + "summary": "Create a security case that uses a Jira connector.", + "value": { + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants.", + "title": "This case will self-destruct in 5 seconds", + "tags": [ + "phishing", + "social engineering" + ], + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "priority": "High", + "parent": null + } + }, + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution" + } + }, + "create_case_response": { + "summary": "The create case API returns a JSON object that includes the user who created the case and the case identifier, version, and creation time.", + "value": { + "id": "66b9aa00-94fa-11ea-9f74-e7e108796192", + "version": "WzUzMiwxXQ==", + "comments": [], + "totalComment": 0, + "totalAlerts": 0, + "title": "This case will self-destruct in 5 seconds", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution", + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active", + "closed_at": null, + "closed_by": null, + "created_at": "2022-05-13T09:16:17.416Z", + "created_by": { + "email": "ahunley@imf.usa.gov", + "full_name": "Alan Hunley", + "username": "ahunley" + }, + "status": "open", + "updated_at": null, + "updated_by": null, + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "parent": null, + "priority": "High" + } + }, + "external_service": null + } + }, + "update_case_request": { + "summary": "Update the case description, tags, and connector.", + "value": { + "cases": [ + { + "id": "a18b38a0-71b0-11ea-a0b2-c51ea50a58e2", + "version": "WzIzLDFd", + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "priority": null, + "parent": null + } + }, + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active!", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + } + } + ] + } + }, + "update_case_response": { + "summary": "This is an example response when the case description, tags, and connector were updated.", + "value": [ + { + "id": "66b9aa00-94fa-11ea-9f74-e7e108796192", + "version": "WzU0OCwxXQ==", + "comments": [], + "totalComment": 0, + "totalAlerts": 0, + "title": "This case will self-destruct in 5 seconds", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution", + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active!", + "closed_at": null, + "closed_by": null, + "created_at": "2022-05-13T09:16:17.416Z", + "created_by": { + "email": "ahunley@imf.usa.gov", + "full_name": "Alan Hunley", + "username": "ahunley" + }, + "status": "open", + "updated_at": "2022-05-13T09:48:33.043Z", + "updated_by": { + "email": "classified@hms.oo.gov.uk", + "full_name": "Classified", + "username": "M" + }, + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "parent": null, + "priority": null + } + }, + "external_service": { + "external_title": "IS-4", + "pushed_by": { + "full_name": "Classified", + "email": "classified@hms.oo.gov.uk", + "username": "M" + }, + "external_url": "https://hms.atlassian.net/browse/IS-4", + "pushed_at": "2022-05-13T09:20:40.672Z", + "connector_id": "05da469f-1fde-4058-99a3-91e4807e2de8", + "external_id": "10003", + "connector_name": "Jira" + } + } + ] + } + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "apiKeyAuth": [] + } + ] +} \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/bundled.yaml b/x-pack/plugins/cases/docs/openapi/bundled.yaml new file mode 100644 index 0000000000000..afad92f489a74 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/bundled.yaml @@ -0,0 +1,1811 @@ +openapi: 3.0.1 +info: + title: Cases + description: OpenAPI schema for Cases endpoints + version: '0.1' + contact: + name: Cases Team + license: + name: Elastic License 2.0 + url: https://www.elastic.co/licensing/elastic-license +tags: + - name: cases + description: Case APIs enable you to open and track issues. + - name: kibana + description: Kibana APIs enable you to interact with Kibana features. +servers: + - url: http://localhost:5601 + description: local +paths: + /api/cases: + post: + description: > + Creates a case. You must have all privileges for the **Cases** feature + in the **Management**, **Observability**, or **Security** section of the + Kibana feature privileges, depending on the owner of the case you're + creating. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM and + ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type is + sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM Resilient + connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for ServiceNow + SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow ITSM + connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + description: + description: The description for the case. + type: string + owner: + $ref: '#/components/schemas/owners' + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + tags: + description: >- + The words and phrases that help categorize cases. It can be + an empty array. + type: array + items: + type: string + title: + description: A title for the case. + type: string + required: + - connector + - description + - owner + - settings + - tags + - title + examples: + createCaseRequest: + $ref: '#/components/examples/create_case_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + closed_at: + type: string + format: date-time + nullable: true + example: null + closed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + comments: + type: array + items: + type: string + example: [] + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-05-13T09:16:17.416Z' + created_by: + type: object + properties: + email: + type: string + example: ahunley@imf.usa.gov + full_name: + type: string + example: Alan Hunley + username: + type: string + example: ahunley + description: + type: string + example: >- + James Bond clicked on a highly suspicious email banner + advertising cheap holidays for underpaid civil servants. + Operation bubblegum is active. Repeat - operation + bubblegum is now active + external_service: + type: object + properties: + connector_id: + type: string + connector_name: + type: string + external_id: + type: string + external_title: + type: string + external_url: + type: string + pushed_at: + type: string + format: date-time + pushed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + id: + type: string + example: 66b9aa00-94fa-11ea-9f74-e7e108796192 + owner: + $ref: '#/components/schemas/owners' + settings: + type: object + properties: + syncAlerts: + type: boolean + example: true + status: + $ref: '#/components/schemas/status' + tags: + type: array + items: + type: string + example: + - phishing + - social engineering + - bubblegum + title: + type: string + example: This case will self-destruct in 5 seconds + totalAlerts: + type: integer + example: 0 + totalComment: + type: integer + example: 0 + updated_at: + type: string + format: date-time + nullable: true + example: null + updated_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + version: + type: string + example: WzUzMiwxXQ== + examples: + createCaseResponse: + $ref: '#/components/examples/create_case_response' + servers: + - url: https://localhost:5601 + delete: + description: > + Deletes one or more cases. You must have all privileges for the + **Cases** feature in the **Management**, **Observability**, or + **Security** section of the Kibana feature privileges, depending on the + owner of the cases you're deleting. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - name: ids + description: >- + The cases that you want to removed. To retrieve case IDs, use the + find cases API. All non-ASCII characters must be URL encoded. + in: query + required: true + schema: + type: string + example: d4e7abb0-b462-11ec-9a8d-698504725a43 + responses: + '204': + description: Indicates a successful call. + servers: + - url: https://localhost:5601 + patch: + description: > + Updates one or more cases. You must have all privileges for the + **Cases** feature in the **Management**, **Observability**, or + **Security** section of the Kibana feature privileges, depending on the + owner of the case you're updating. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: + cases: + type: array + items: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + fields: + description: >- + An object containing the connector fields. To + create a case without a connector, specify null. + If you want to omit any individual field, specify + null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow + ITSM and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: >- + The type of incident for IBM Resilient + connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue + type is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and + ServiceNow SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow + ITSM connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution + can be delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case + without a connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + description: + description: The description for the case. + type: string + id: + description: The identifier for the case. + type: string + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + status: + $ref: '#/components/schemas/status' + tags: + description: The words and phrases that help categorize cases. + type: array + items: + type: string + title: + description: A title for the case. + type: string + version: + description: The current version of the case. + type: string + required: + - id + - version + examples: + updateCaseRequest: + $ref: '#/components/examples/update_case_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + closed_at: + type: string + format: date-time + nullable: true + example: null + closed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + comments: + type: array + items: + type: string + example: [] + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-05-13T09:16:17.416Z' + created_by: + type: object + properties: + email: + type: string + example: ahunley@imf.usa.gov + full_name: + type: string + example: Alan Hunley + username: + type: string + example: ahunley + description: + type: string + example: >- + James Bond clicked on a highly suspicious email banner + advertising cheap holidays for underpaid civil servants. + Operation bubblegum is active. Repeat - operation + bubblegum is now active + external_service: + type: object + properties: + connector_id: + type: string + connector_name: + type: string + external_id: + type: string + external_title: + type: string + external_url: + type: string + pushed_at: + type: string + format: date-time + pushed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + id: + type: string + example: 66b9aa00-94fa-11ea-9f74-e7e108796192 + owner: + $ref: '#/components/schemas/owners' + settings: + type: object + properties: + syncAlerts: + type: boolean + example: true + status: + $ref: '#/components/schemas/status' + tags: + type: array + items: + type: string + example: + - phishing + - social engineering + - bubblegum + title: + type: string + example: This case will self-destruct in 5 seconds + totalAlerts: + type: integer + example: 0 + totalComment: + type: integer + example: 0 + updated_at: + type: string + format: date-time + nullable: true + example: null + updated_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + version: + type: string + example: WzUzMiwxXQ== + examples: + updateCaseResponse: + $ref: '#/components/examples/update_case_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/cases: + post: + description: > + Creates a case. You must have all privileges for the **Cases** feature + in the **Management**, **Observability**, or **Security** section of the + Kibana feature privileges, depending on the owner of the case you're + creating. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + requestBody: + content: + application/json: + schema: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM and + ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type is + sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM Resilient + connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for ServiceNow + SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow ITSM + connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + description: + description: The description for the case. + type: string + owner: + $ref: '#/components/schemas/owners' + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + tags: + description: >- + The words and phrases that help categorize cases. It can be + an empty array. + type: array + items: + type: string + title: + description: A title for the case. + type: string + required: + - connector + - description + - owner + - settings + - tags + - title + examples: + createCaseRequest: + $ref: '#/components/examples/create_case_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + closed_at: + type: string + format: date-time + nullable: true + example: null + closed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + comments: + type: array + items: + type: string + example: [] + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-05-13T09:16:17.416Z' + created_by: + type: object + properties: + email: + type: string + example: ahunley@imf.usa.gov + full_name: + type: string + example: Alan Hunley + username: + type: string + example: ahunley + description: + type: string + example: >- + James Bond clicked on a highly suspicious email banner + advertising cheap holidays for underpaid civil servants. + Operation bubblegum is active. Repeat - operation + bubblegum is now active + external_service: + type: object + properties: + connector_id: + type: string + connector_name: + type: string + external_id: + type: string + external_title: + type: string + external_url: + type: string + pushed_at: + type: string + format: date-time + pushed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + id: + type: string + example: 66b9aa00-94fa-11ea-9f74-e7e108796192 + owner: + $ref: '#/components/schemas/owners' + settings: + type: object + properties: + syncAlerts: + type: boolean + example: true + status: + $ref: '#/components/schemas/status' + tags: + type: array + items: + type: string + example: + - phishing + - social engineering + - bubblegum + title: + type: string + example: This case will self-destruct in 5 seconds + totalAlerts: + type: integer + example: 0 + totalComment: + type: integer + example: 0 + updated_at: + type: string + format: date-time + nullable: true + example: null + updated_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + version: + type: string + example: WzUzMiwxXQ== + examples: + createCaseResponse: + $ref: '#/components/examples/create_case_response' + servers: + - url: https://localhost:5601 + delete: + description: > + Deletes one or more cases. You must have all privileges for the + **Cases** feature in the **Management**, **Observability**, or + **Security** section of the Kibana feature privileges, depending on the + owner of the cases you're deleting. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - name: ids + description: >- + The cases that you want to removed. All non-ASCII characters must be + URL encoded. + in: query + required: true + schema: + type: string + example: d4e7abb0-b462-11ec-9a8d-698504725a43 + responses: + '204': + description: Indicates a successful call. + servers: + - url: https://localhost:5601 + patch: + description: > + Updates one or more cases. You must have all privileges for the + **Cases** feature in the **Management**, **Observability**, or + **Security** section of the Kibana feature privileges, depending on the + owner of the case you're updating. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + requestBody: + content: + application/json: + schema: + type: object + properties: + cases: + type: array + items: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + fields: + description: >- + An object containing the connector fields. To + create a case without a connector, specify null. + If you want to omit any individual field, specify + null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow + ITSM and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: >- + The type of incident for IBM Resilient + connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue + type is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and + ServiceNow SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow + ITSM connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution + can be delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case + without a connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + description: + description: The description for the case. + type: string + id: + description: The identifier for the case. + type: string + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + status: + $ref: '#/components/schemas/status' + tags: + description: The words and phrases that help categorize cases. + type: array + items: + type: string + title: + description: A title for the case. + type: string + version: + description: The current version of the case. + type: string + required: + - id + - version + examples: + updateCaseRequest: + $ref: '#/components/examples/update_case_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + closed_at: + type: string + format: date-time + nullable: true + example: null + closed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + comments: + type: array + items: + type: string + example: [] + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-05-13T09:16:17.416Z' + created_by: + type: object + properties: + email: + type: string + example: ahunley@imf.usa.gov + full_name: + type: string + example: Alan Hunley + username: + type: string + example: ahunley + description: + type: string + example: >- + James Bond clicked on a highly suspicious email banner + advertising cheap holidays for underpaid civil servants. + Operation bubblegum is active. Repeat - operation + bubblegum is now active + external_service: + type: object + properties: + connector_id: + type: string + connector_name: + type: string + external_id: + type: string + external_title: + type: string + external_url: + type: string + pushed_at: + type: string + format: date-time + pushed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + id: + type: string + example: 66b9aa00-94fa-11ea-9f74-e7e108796192 + owner: + $ref: '#/components/schemas/owners' + settings: + type: object + properties: + syncAlerts: + type: boolean + example: true + status: + $ref: '#/components/schemas/status' + tags: + type: array + items: + type: string + example: + - phishing + - social engineering + - bubblegum + title: + type: string + example: This case will self-destruct in 5 seconds + totalAlerts: + type: integer + example: 0 + totalComment: + type: integer + example: 0 + updated_at: + type: string + format: date-time + nullable: true + example: null + updated_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + version: + type: string + example: WzUzMiwxXQ== + examples: + updateCaseResponse: + $ref: '#/components/examples/update_case_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + apiKeyAuth: + type: apiKey + in: header + name: ApiKey + parameters: + kbn_xsrf: + schema: + type: string + in: header + name: kbn-xsrf + required: true + space_id: + in: path + name: spaceId + description: An identifier for the space. + required: true + schema: + type: string + example: default + schemas: + connector_types: + type: string + description: The type of connector. + enum: + - .jira + - .none + - .resilient + - .servicenow + - .servicenow-sir + - .swimlane + owners: + type: string + description: Owner apps + enum: + - cases + - observability + - securitySolution + status: + type: string + description: The status of the case. + enum: + - closed + - in-progress + - open + examples: + create_case_request: + summary: Create a security case that uses a Jira connector. + value: + description: >- + James Bond clicked on a highly suspicious email banner advertising + cheap holidays for underpaid civil servants. + title: This case will self-destruct in 5 seconds + tags: + - phishing + - social engineering + connector: + id: 131d4448-abe0-4789-939d-8ef60680b498 + name: My connector + type: .jira + fields: + issueType: '10006' + priority: High + parent: null + settings: + syncAlerts: true + owner: securitySolution + create_case_response: + summary: >- + The create case API returns a JSON object that includes the user who + created the case and the case identifier, version, and creation time. + value: + id: 66b9aa00-94fa-11ea-9f74-e7e108796192 + version: WzUzMiwxXQ== + comments: [] + totalComment: 0 + totalAlerts: 0 + title: This case will self-destruct in 5 seconds + tags: + - phishing + - social engineering + - bubblegum + settings: + syncAlerts: true + owner: securitySolution + description: >- + James Bond clicked on a highly suspicious email banner advertising + cheap holidays for underpaid civil servants. Operation bubblegum is + active. Repeat - operation bubblegum is now active + closed_at: null + closed_by: null + created_at: '2022-05-13T09:16:17.416Z' + created_by: + email: ahunley@imf.usa.gov + full_name: Alan Hunley + username: ahunley + status: open + updated_at: null + updated_by: null + connector: + id: 131d4448-abe0-4789-939d-8ef60680b498 + name: My connector + type: .jira + fields: + issueType: '10006' + parent: null + priority: High + external_service: null + update_case_request: + summary: Update the case description, tags, and connector. + value: + cases: + - id: a18b38a0-71b0-11ea-a0b2-c51ea50a58e2 + version: WzIzLDFd + connector: + id: 131d4448-abe0-4789-939d-8ef60680b498 + name: My connector + type: .jira + fields: + issueType: '10006' + priority: null + parent: null + description: >- + James Bond clicked on a highly suspicious email banner advertising + cheap holidays for underpaid civil servants. Operation bubblegum + is active. Repeat - operation bubblegum is now active! + tags: + - phishing + - social engineering + - bubblegum + settings: + syncAlerts: true + update_case_response: + summary: >- + This is an example response when the case description, tags, and + connector were updated. + value: + - id: 66b9aa00-94fa-11ea-9f74-e7e108796192 + version: WzU0OCwxXQ== + comments: [] + totalComment: 0 + totalAlerts: 0 + title: This case will self-destruct in 5 seconds + tags: + - phishing + - social engineering + - bubblegum + settings: + syncAlerts: true + owner: securitySolution + description: >- + James Bond clicked on a highly suspicious email banner advertising + cheap holidays for underpaid civil servants. Operation bubblegum is + active. Repeat - operation bubblegum is now active! + closed_at: null + closed_by: null + created_at: '2022-05-13T09:16:17.416Z' + created_by: + email: ahunley@imf.usa.gov + full_name: Alan Hunley + username: ahunley + status: open + updated_at: '2022-05-13T09:48:33.043Z' + updated_by: + email: classified@hms.oo.gov.uk + full_name: Classified + username: M + connector: + id: 131d4448-abe0-4789-939d-8ef60680b498 + name: My connector + type: .jira + fields: + issueType: '10006' + parent: null + priority: null + external_service: + external_title: IS-4 + pushed_by: + full_name: Classified + email: classified@hms.oo.gov.uk + username: M + external_url: https://hms.atlassian.net/browse/IS-4 + pushed_at: '2022-05-13T09:20:40.672Z' + connector_id: 05da469f-1fde-4058-99a3-91e4807e2de8 + external_id: '10003' + connector_name: Jira +security: + - basicAuth: [] + - apiKeyAuth: [] diff --git a/x-pack/plugins/cases/docs/openapi/components/README.md b/x-pack/plugins/cases/docs/openapi/components/README.md new file mode 100644 index 0000000000000..0841562a33150 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/README.md @@ -0,0 +1,7 @@ +Reusable components +=========== + + - `examples` - reusable [Example objects](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#exampleObject) + - `headers` - reusable [Header objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#headerObject) + - `parameters` - reusable [Parameter objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject) + - `schemas` - reusable [Schema objects](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#schemaObject) diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/create_case_request.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/create_case_request.yaml new file mode 100644 index 0000000000000..0659ed18a8569 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/examples/create_case_request.yaml @@ -0,0 +1,21 @@ +summary: Create a security case that uses a Jira connector. +value: + { + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants.", + "title": "This case will self-destruct in 5 seconds", + "tags": [ "phishing","social engineering"], + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "priority": "High", + "parent": null + } + }, + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution" + } diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml new file mode 100644 index 0000000000000..f9f2ce3d61beb --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml @@ -0,0 +1,42 @@ +summary: The create case API returns a JSON object that includes the user who created the case and the case identifier, version, and creation time. +value: + { + "id": "66b9aa00-94fa-11ea-9f74-e7e108796192", + "version": "WzUzMiwxXQ==", + "comments": [], + "totalComment": 0, + "totalAlerts": 0, + "title": "This case will self-destruct in 5 seconds", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution", + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active", + "closed_at": null, + "closed_by": null, + "created_at": "2022-05-13T09:16:17.416Z", + "created_by": { + "email": "ahunley@imf.usa.gov", + "full_name": "Alan Hunley", + "username": "ahunley" + }, + "status": "open", + "updated_at": null, + "updated_by": null, + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "parent": null, + "priority": "High" + } + }, + "external_service": null + } diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/update_case_request.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/update_case_request.yaml new file mode 100644 index 0000000000000..7ecb306cf0735 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/examples/update_case_request.yaml @@ -0,0 +1,29 @@ +summary: Update the case description, tags, and connector. +value: + { + "cases": [ + { + "id": "a18b38a0-71b0-11ea-a0b2-c51ea50a58e2", + "version": "WzIzLDFd", + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "priority": null, + "parent": null + } + }, + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active!", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + } + } + ] +} \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml new file mode 100644 index 0000000000000..a73191868c8ee --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml @@ -0,0 +1,60 @@ +summary: This is an example response when the case description, tags, and connector were updated. +value: + [ + { + "id": "66b9aa00-94fa-11ea-9f74-e7e108796192", + "version": "WzU0OCwxXQ==", + "comments": [], + "totalComment": 0, + "totalAlerts": 0, + "title": "This case will self-destruct in 5 seconds", + "tags": [ + "phishing", + "social engineering", + "bubblegum" + ], + "settings": { + "syncAlerts": true + }, + "owner": "securitySolution", + "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active!", + "closed_at": null, + "closed_by": null, + "created_at": "2022-05-13T09:16:17.416Z", + "created_by": { + "email": "ahunley@imf.usa.gov", + "full_name": "Alan Hunley", + "username": "ahunley" + }, + "status": "open", + "updated_at": "2022-05-13T09:48:33.043Z", + "updated_by": { + "email": "classified@hms.oo.gov.uk", + "full_name": "Classified", + "username": "M" + }, + "connector": { + "id": "131d4448-abe0-4789-939d-8ef60680b498", + "name": "My connector", + "type": ".jira", + "fields": { + "issueType": "10006", + "parent": null, + "priority": null, + } + }, + "external_service": { + "external_title": "IS-4", + "pushed_by": { + "full_name": "Classified", + "email": "classified@hms.oo.gov.uk", + "username": "M" + }, + "external_url": "https://hms.atlassian.net/browse/IS-4", + "pushed_at": "2022-05-13T09:20:40.672Z", + "connector_id": "05da469f-1fde-4058-99a3-91e4807e2de8", + "external_id": "10003", + "connector_name": "Jira" + } + } + ] \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/headers/kbn_xsrf.yaml b/x-pack/plugins/cases/docs/openapi/components/headers/kbn_xsrf.yaml new file mode 100644 index 0000000000000..3d8dfae634e68 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/headers/kbn_xsrf.yaml @@ -0,0 +1,5 @@ +schema: + type: string +in: header +name: kbn-xsrf +required: true diff --git a/x-pack/plugins/cases/docs/openapi/components/parameters/space_id.yaml b/x-pack/plugins/cases/docs/openapi/components/parameters/space_id.yaml new file mode 100644 index 0000000000000..0ff325b08a082 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/parameters/space_id.yaml @@ -0,0 +1,7 @@ +in: path +name: spaceId +description: An identifier for the space. +required: true +schema: + type: string + example: default diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml new file mode 100644 index 0000000000000..780496f1591b4 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml @@ -0,0 +1,117 @@ +closed_at: + type: string + format: date-time + nullable: true + example: null +closed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null +comments: + type: array + items: + type: string + example: [] +connector: + type: object + properties: + $ref: 'connector_properties.yaml' +created_at: + type: string + format: date-time + example: "2022-05-13T09:16:17.416Z" +created_by: + type: object + properties: + email: + type: string + example: "ahunley@imf.usa.gov" + full_name: + type: string + example: "Alan Hunley" + username: + type: string + example: "ahunley" +description: + type: string + example: "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" +external_service: + type: object + properties: + connector_id: + type: string + connector_name: + type: string + external_id: + type: string + external_title: + type: string + external_url: + type: string + pushed_at: + type: string + format: date-time + pushed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null +id: + type: string + example: "66b9aa00-94fa-11ea-9f74-e7e108796192" +owner: + $ref: 'owners.yaml' +settings: + type: object + properties: + syncAlerts: + type: boolean + example: true +status: + $ref: 'status.yaml' +tags: + type: array + items: + type: string + example: ["phishing","social engineering","bubblegum"] +title: + type: string + example: "This case will self-destruct in 5 seconds" +totalAlerts: + type: integer + example: 0 +totalComment: + type: integer + example: 0 +updated_at: + type: string + format: date-time + nullable: true + example: null +updated_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null +version: + type: string + example: "WzUzMiwxXQ==" diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/closure_types.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/closure_types.yaml new file mode 100644 index 0000000000000..f09063d0db18f --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/closure_types.yaml @@ -0,0 +1,5 @@ +type: string +description: Indicates whether a case is automatically closed when it is pushed to external systems (`close-by-pushing`) or not automatically closed (`close-by-user`). +enum: + - close-by-pushing + - close-by-user \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/comment_types.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/comment_types.yaml new file mode 100644 index 0000000000000..a6a86ae163b20 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/comment_types.yaml @@ -0,0 +1,5 @@ +type: string +description: The type of comment. +enum: + - alert + - user \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/connector_properties.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/connector_properties.yaml new file mode 100644 index 0000000000000..c2bc2ab7c887a --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/connector_properties.yaml @@ -0,0 +1,65 @@ +fields: + description: An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors. + type: string + destIp: + description: A comma-separated list of destination IPs for ServiceNow SecOps connectors. + type: string + impact: + description: The effect an incident had on business for ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: A comma-separated list of malware hashes for ServiceNow SecOps connectors. + type: string + malwareUrl: + description: A comma-separated list of malware URLs for ServiceNow SecOps connectors. + type: string + parent: + description: The key of the parent issue, when the issue type is sub-task for Jira connectors. + type: string + priority: + description: The priority of the issue for Jira and ServiceNow SecOps connectors. + type: string + severity: + description: The severity of the incident for ServiceNow ITSM connectors. + type: string + severityCode: + description: The severity code of the incident for IBM Resilient connectors. + type: number + sourceIp: + description: A comma-separated list of source IPs for ServiceNow SecOps connectors. + type: string + subcategory: + description: The subcategory of the incident for ServiceNow ITSM connectors. + type: string + urgency: + description: The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors. + type: string + required: + - fields + - id + - name + - type +id: + description: The identifier for the connector. To create a case without a connector, use `none`. + type: string +name: + description: The name of the connector. To create a case without a connector, use `none`. + type: string +type: + $ref: 'connector_types.yaml' \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/connector_types.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/connector_types.yaml new file mode 100644 index 0000000000000..24c1ec5880828 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/connector_types.yaml @@ -0,0 +1,9 @@ +type: string +description: The type of connector. +enum: + - .jira + - .none + - .resilient + - .servicenow + - .servicenow-sir + - .swimlane \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/owners.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/owners.yaml new file mode 100644 index 0000000000000..f39324a36e702 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/owners.yaml @@ -0,0 +1,6 @@ +type: string +description: Owner apps +enum: + - cases + - observability + - securitySolution \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/status.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/status.yaml new file mode 100644 index 0000000000000..1fe2e342dd776 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/status.yaml @@ -0,0 +1,6 @@ +type: string +description: The status of the case. +enum: + - closed + - in-progress + - open \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/entrypoint.yaml b/x-pack/plugins/cases/docs/openapi/entrypoint.yaml new file mode 100644 index 0000000000000..14155c156b0cc --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/entrypoint.yaml @@ -0,0 +1,92 @@ +openapi: 3.0.1 +info: + title: Cases + description: OpenAPI schema for Cases endpoints + version: '0.1' + contact: + name: Cases Team + license: + name: Elastic License 2.0 + url: https://www.elastic.co/licensing/elastic-license +tags: + - name: cases + description: Case APIs enable you to open and track issues. + - name: kibana + description: Kibana APIs enable you to interact with Kibana features. +servers: + - url: 'http://localhost:5601' + description: local +paths: + /api/cases: + $ref: paths/api@cases.yaml +# /api/cases/_find: +# $ref: paths/api@cases@_find.yaml +# '/api/cases/alerts/{alertId}': +# $ref: 'paths/api@cases@alerts@{alertid}.yaml' +# '/api/cases/configure': +# $ref: paths/api@cases@configure.yaml +# '/api/cases/configure/{configurationId}': +# $ref: paths/api@cases@configure@{configurationid}.yaml +# '/api/cases/configure/connectors/_find': +# $ref: paths/api@cases@configure@connectors@_find.yaml +# '/api/cases/reporters': +# $ref: 'paths/api@cases@reporters.yaml' +# '/api/cases/status': +# $ref: 'paths/api@cases@status.yaml' +# '/api/cases/tags': +# $ref: 'paths/api@cases@tags.yaml' +# '/api/cases/{caseId}': +# $ref: 'paths/api@cases@{caseid}.yaml' +# '/api/cases/{caseId}/alerts': +# $ref: 'paths/api@cases@{caseid}@alerts.yaml' +# '/api/cases/{caseId}/comments': +# $ref: 'paths/api@cases@{caseid}@comments.yaml' +# '/api/cases/{caseId}/comments/{commentId}': +# $ref: 'paths/api@cases@{caseid}@comments@{commentid}.yaml' +# '/api/cases/{caseId}/connector/{connectorId}/_push': +# $ref: 'paths/api@cases@{caseid}@connector@{connectorid}@_push.yaml' +# '/api/cases/{caseId}/user_actions': +# $ref: 'paths/api@cases@{caseid}@user_actions.yaml' + + '/s/{spaceId}/api/cases': + $ref: 'paths/s@{spaceid}@api@cases.yaml' + # '/s/{spaceId}/api/cases/_find': + # $ref: 'paths/s@{spaceid}@api@cases@_find.yaml' + # '/s/{spaceId}/api/cases/alerts/{alertId}': + # $ref: 'paths/s@{spaceid}@api@cases@alerts@{alertid}.yaml' + # '/s/{spaceId}/api/cases/configure': + # $ref: paths/s@{spaceid}@api@cases@configure.yaml + # '/s/{spaceId}/api/cases/configure/{configurationId}': + # $ref: paths/s@{spaceid}@api@cases@configure@{configurationid}.yaml + # '/s/{spaceId}/api/cases/configure/connectors/_find': + # $ref: paths/s@{spaceid}@api@cases@configure@connectors@_find.yaml + # '/s/{spaceId}/api/cases/reporters': + # $ref: 'paths/s@{spaceid}@api@cases@reporters.yaml' + # '/s/{spaceId}/api/cases/status': + # $ref: 'paths/s@{spaceid}@api@cases@status.yaml' + # '/s/{spaceId}/api/cases/tags': + # $ref: 'paths/s@{spaceid}@api@cases@tags.yaml' + # '/s/{spaceId}/api/cases/{caseId}': + # $ref: 'paths/s@{spaceid}@api@cases@{caseid}.yaml' + # '/s/{spaceId}/api/cases/{caseId}/alerts': + # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@alerts.yaml' + # '/s/{spaceId}/api/cases/{caseId}/comments': + # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments.yaml' + # '/s/{spaceId}/api/cases/{caseId}/comments/{commentId}': + # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments@{commentid}.yaml' + # '/s/{spaceId}/api/cases/{caseId}/connector/{connectorId}/_push': + # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@connector@{connectorid}@_push.yaml' + # '/s/{spaceId}/api/cases/{caseId}/user_actions': + # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@user_actions.yaml' +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + apiKeyAuth: + type: apiKey + in: header + name: ApiKey +security: + - basicAuth: [] + - apiKeyAuth: [] diff --git a/x-pack/plugins/cases/docs/openapi/paths/README.md b/x-pack/plugins/cases/docs/openapi/paths/README.md new file mode 100644 index 0000000000000..b7818c8474fc8 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/paths/README.md @@ -0,0 +1,10 @@ +Paths +===== + +Each path definition for which there is a specification exists within this folder. + +These files currently use the following conventions: + +* path separator token (e.g. `@`) is included in the file name +* path parameter (e.g. `{example}`) is included in the file name +* there is one file per path; each file can contain multiple operations diff --git a/x-pack/plugins/cases/docs/openapi/paths/api@cases.yaml b/x-pack/plugins/cases/docs/openapi/paths/api@cases.yaml new file mode 100644 index 0000000000000..c37bb3ecef645 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/paths/api@cases.yaml @@ -0,0 +1,161 @@ +post: + description: > + Creates a case. + You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're creating. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + $ref: '../components/schemas/connector_properties.yaml' + description: + description: The description for the case. + type: string + owner: + $ref: '../components/schemas/owners.yaml' + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + tags: + description: The words and phrases that help categorize cases. It can be an empty array. + type: array + items: + type: string + title: + description: A title for the case. + type: string + required: + - connector + - description + - owner + - settings + - tags + - title + examples: + createCaseRequest: + $ref: '../components/examples/create_case_request.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + $ref: '../components/schemas/case_response_properties.yaml' + examples: + createCaseResponse: + $ref: '../components/examples/create_case_response.yaml' + servers: + - url: https://localhost:5601 + +delete: + description: > + Deletes one or more cases. + You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're deleting. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - name: ids + description: The cases that you want to removed. To retrieve case IDs, use the find cases API. All non-ASCII characters must be URL encoded. + in: query + required: true + schema: + type: string + example: 'd4e7abb0-b462-11ec-9a8d-698504725a43' + responses: + '204': + description: Indicates a successful call. + servers: + - url: https://localhost:5601 + +patch: + description: > + Updates one or more cases. + You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + cases: + type: array + items: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + $ref: '../components/schemas/connector_properties.yaml' + description: + description: The description for the case. + type: string + id: + description: The identifier for the case. + type: string + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + status: + $ref: '../components/schemas/status.yaml' + tags: + description: The words and phrases that help categorize cases. + type: array + items: + type: string + title: + description: A title for the case. + type: string + version: + description: The current version of the case. + type: string + required: + - id + - version + examples: + updateCaseRequest: + $ref: '../components/examples/update_case_request.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + $ref: '../components/schemas/case_response_properties.yaml' + examples: + updateCaseResponse: + $ref: '../components/examples/update_case_response.yaml' + servers: + - url: https://localhost:5601 + +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases.yaml b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases.yaml new file mode 100644 index 0000000000000..c03ea64a53538 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases.yaml @@ -0,0 +1,164 @@ +post: + description: > + Creates a case. + You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're creating. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + requestBody: + content: + application/json: + schema: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + $ref: '../components/schemas/connector_properties.yaml' + description: + description: The description for the case. + type: string + owner: + $ref: '../components/schemas/owners.yaml' + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + tags: + description: The words and phrases that help categorize cases. It can be an empty array. + type: array + items: + type: string + title: + description: A title for the case. + type: string + required: + - connector + - description + - owner + - settings + - tags + - title + examples: + createCaseRequest: + $ref: '../components/examples/create_case_request.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + $ref: '../components/schemas/case_response_properties.yaml' + examples: + createCaseResponse: + $ref: '../components/examples/create_case_response.yaml' + servers: + - url: https://localhost:5601 + +delete: + description: > + Deletes one or more cases. + You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're deleting. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + - name: ids + description: The cases that you want to removed. All non-ASCII characters must be URL encoded. + in: query + required: true + schema: + type: string + example: 'd4e7abb0-b462-11ec-9a8d-698504725a43' + responses: + '204': + description: Indicates a successful call. + servers: + - url: https://localhost:5601 + +patch: + description: > + Updates one or more cases. + You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + requestBody: + content: + application/json: + schema: + type: object + properties: + cases: + type: array + items: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + $ref: '../components/schemas/connector_properties.yaml' + description: + description: The description for the case. + type: string + id: + description: The identifier for the case. + type: string + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + status: + $ref: '../components/schemas/status.yaml' + tags: + description: The words and phrases that help categorize cases. + type: array + items: + type: string + title: + description: A title for the case. + type: string + version: + description: The current version of the case. + type: string + required: + - id + - version + examples: + updateCaseRequest: + $ref: '../components/examples/update_case_request.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + $ref: '../components/schemas/case_response_properties.yaml' + examples: + updateCaseResponse: + $ref: '../components/examples/update_case_response.yaml' + servers: + - url: https://localhost:5601 + +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/cases/public/api/__mocks__/index.ts b/x-pack/plugins/cases/public/api/__mocks__/index.ts new file mode 100644 index 0000000000000..a1df651240224 --- /dev/null +++ b/x-pack/plugins/cases/public/api/__mocks__/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CasesFindRequest } from '../../../common/api'; +import { HTTPService } from '..'; +import { casesStatus } from '../../containers/mock'; +import { CasesStatus } from '../../containers/types'; + +export const getCasesStatus = async ({ + http, + signal, + query, +}: HTTPService & { query: CasesFindRequest }): Promise => Promise.resolve(casesStatus); diff --git a/x-pack/plugins/cases/public/api/decoders.ts b/x-pack/plugins/cases/public/api/decoders.ts new file mode 100644 index 0000000000000..6b9cfdb21742c --- /dev/null +++ b/x-pack/plugins/cases/public/api/decoders.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { createToasterPlainError } from '../containers/utils'; +import { throwErrors } from '../../common'; +import { + CasesFindResponse, + CasesFindResponseRt, + CasesStatusResponse, + CasesStatusResponseRt, + CasesMetricsResponse, + CasesMetricsResponseRt, +} from '../../common/api'; + +export const decodeCasesFindResponse = (respCases?: CasesFindResponse) => + pipe(CasesFindResponseRt.decode(respCases), fold(throwErrors(createToasterPlainError), identity)); + +export const decodeCasesStatusResponse = (respCase?: CasesStatusResponse) => + pipe( + CasesStatusResponseRt.decode(respCase), + fold(throwErrors(createToasterPlainError), identity) + ); + +export const decodeCasesMetricsResponse = (metrics?: CasesMetricsResponse) => + pipe( + CasesMetricsResponseRt.decode(metrics), + fold(throwErrors(createToasterPlainError), identity) + ); diff --git a/x-pack/plugins/cases/public/api/index.test.ts b/x-pack/plugins/cases/public/api/index.test.ts new file mode 100644 index 0000000000000..321a1db206846 --- /dev/null +++ b/x-pack/plugins/cases/public/api/index.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '@kbn/core/public/mocks'; +import { getCases, getCasesMetrics } from '.'; +import { allCases, allCasesSnake } from '../containers/mock'; + +describe('api', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getCases', () => { + const http = httpServiceMock.createStartContract({ basePath: '' }); + http.get.mockResolvedValue(allCasesSnake); + + it('should return the correct response', async () => { + expect(await getCases({ http, query: { from: 'now-1d' } })).toEqual(allCases); + }); + + it('should have been called with the correct path', async () => { + await getCases({ http, query: { perPage: 10 } }); + expect(http.get).toHaveBeenCalledWith('/api/cases/_find', { + query: { perPage: 10 }, + }); + }); + }); + + describe('getCasesMetrics', () => { + const http = httpServiceMock.createStartContract({ basePath: '' }); + http.get.mockResolvedValue({ mttr: 0 }); + + it('should return the correct response', async () => { + expect( + await getCasesMetrics({ http, query: { features: ['mttr'], from: 'now-1d' } }) + ).toEqual({ mttr: 0 }); + }); + + it('should have been called with the correct path', async () => { + await getCasesMetrics({ http, query: { features: ['mttr'], to: 'now-1d' } }); + expect(http.get).toHaveBeenCalledWith('/api/cases/metrics', { + query: { features: ['mttr'], to: 'now-1d' }, + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/api/index.ts b/x-pack/plugins/cases/public/api/index.ts new file mode 100644 index 0000000000000..209c0ad619d4b --- /dev/null +++ b/x-pack/plugins/cases/public/api/index.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpStart } from '@kbn/core/public'; +import { Cases, CasesStatus, CasesMetrics } from '../../common/ui'; +import { CASE_FIND_URL, CASE_METRICS_URL, CASE_STATUS_URL } from '../../common/constants'; +import { + CasesFindRequest, + CasesFindResponse, + CasesMetricsRequest, + CasesMetricsResponse, + CasesStatusRequest, + CasesStatusResponse, +} from '../../common/api'; +import { convertAllCasesToCamel, convertToCamelCase } from './utils'; +import { + decodeCasesFindResponse, + decodeCasesMetricsResponse, + decodeCasesStatusResponse, +} from './decoders'; + +export interface HTTPService { + http: HttpStart; + signal?: AbortSignal; +} + +export const getCases = async ({ + http, + signal, + query, +}: HTTPService & { query: CasesFindRequest }): Promise => { + const res = await http.get(CASE_FIND_URL, { query, signal }); + return convertAllCasesToCamel(decodeCasesFindResponse(res)); +}; + +export const getCasesStatus = async ({ + http, + signal, + query, +}: HTTPService & { query: CasesStatusRequest }): Promise => { + const response = await http.get(CASE_STATUS_URL, { + signal, + query, + }); + + return convertToCamelCase(decodeCasesStatusResponse(response)); +}; + +export const getCasesMetrics = async ({ + http, + signal, + query, +}: HTTPService & { query: CasesMetricsRequest }): Promise => { + const res = await http.get(CASE_METRICS_URL, { signal, query }); + return convertToCamelCase(decodeCasesMetricsResponse(res)); +}; diff --git a/x-pack/plugins/cases/public/api/utils.test.ts b/x-pack/plugins/cases/public/api/utils.test.ts new file mode 100644 index 0000000000000..f46ada35ffca1 --- /dev/null +++ b/x-pack/plugins/cases/public/api/utils.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { allCases, allCasesSnake } from '../containers/mock'; +import { convertAllCasesToCamel, convertArrayToCamelCase, convertToCamelCase } from './utils'; + +describe('utils', () => { + describe('convertArrayToCamelCase', () => { + it('converts an array of items to camel case correctly', () => { + const items = [ + { foo_bar: [{ bar_foo: '1' }], test_bar: '2', obj_pros: { is_valid: true } }, + { bar_test: [{ baz_foo: '1' }], test_bar: '2', obj_pros: { is_valid: true } }, + ]; + expect(convertArrayToCamelCase(items)).toEqual([ + { fooBar: [{ barFoo: '1' }], testBar: '2', objPros: { isValid: true } }, + { barTest: [{ bazFoo: '1' }], testBar: '2', objPros: { isValid: true } }, + ]); + }); + }); + + describe('convertToCamelCase', () => { + it('converts an object to camel case correctly', () => { + const obj = { bar_test: [{ baz_foo: '1' }], test_bar: '2', obj_pros: { is_valid: true } }; + + expect(convertToCamelCase(obj)).toEqual({ + barTest: [{ bazFoo: '1' }], + testBar: '2', + objPros: { isValid: true }, + }); + }); + }); + + describe('convertAllCasesToCamel', () => { + it('converts the find response to camel case', () => { + expect(convertAllCasesToCamel(allCasesSnake)).toEqual(allCases); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/api/utils.ts b/x-pack/plugins/cases/public/api/utils.ts new file mode 100644 index 0000000000000..19cb800de921b --- /dev/null +++ b/x-pack/plugins/cases/public/api/utils.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 { isArray, set, camelCase, isObject } from 'lodash'; +import { CasesFindResponse, CaseResponse } from '../../common/api'; +import { Cases, Case } from '../containers/types'; + +export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => + arrayOfSnakes.reduce((acc: unknown[], value) => { + if (isArray(value)) { + return [...acc, convertArrayToCamelCase(value)]; + } else if (isObject(value)) { + return [...acc, convertToCamelCase(value)]; + } else { + return [...acc, value]; + } + }, []); + +export const convertToCamelCase = (obj: T): U => + Object.entries(obj).reduce((acc, [key, value]) => { + if (isArray(value)) { + set(acc, camelCase(key), convertArrayToCamelCase(value)); + } else if (isObject(value)) { + set(acc, camelCase(key), convertToCamelCase(value)); + } else { + set(acc, camelCase(key), value); + } + return acc; + }, {} as U); + +export const convertAllCasesToCamel = (snakeCases: CasesFindResponse): Cases => ({ + cases: snakeCases.cases.map((theCase) => convertToCamelCase(theCase)), + countOpenCases: snakeCases.count_open_cases, + countInProgressCases: snakeCases.count_in_progress_cases, + countClosedCases: snakeCases.count_closed_cases, + page: snakeCases.page, + perPage: snakeCases.per_page, + total: snakeCases.total, +}); diff --git a/x-pack/plugins/cases/public/client/api/index.test.ts b/x-pack/plugins/cases/public/client/api/index.test.ts index 8bf4ca682fe3b..dacea3350bd4a 100644 --- a/x-pack/plugins/cases/public/client/api/index.test.ts +++ b/x-pack/plugins/cases/public/client/api/index.test.ts @@ -7,7 +7,7 @@ import { httpServiceMock } from '@kbn/core/public/mocks'; import { createClientAPI } from '.'; -import { allCases, casesStatus } from '../../containers/mock'; +import { allCases, allCasesSnake } from '../../containers/mock'; describe('createClientAPI', () => { beforeEach(() => { @@ -48,7 +48,7 @@ describe('createClientAPI', () => { describe('find', () => { const http = httpServiceMock.createStartContract({ basePath: '' }); const api = createClientAPI({ http }); - http.get.mockResolvedValue(allCases); + http.get.mockResolvedValue(allCasesSnake); it('should return the correct response', async () => { expect(await api.cases.find({ from: 'now-1d' })).toEqual(allCases); @@ -62,19 +62,21 @@ describe('createClientAPI', () => { }); }); - describe('getAllCasesMetrics', () => { + describe('getCasesMetrics', () => { const http = httpServiceMock.createStartContract({ basePath: '' }); const api = createClientAPI({ http }); - http.get.mockResolvedValue(casesStatus); + http.get.mockResolvedValue({ mttr: 0 }); it('should return the correct response', async () => { - expect(await api.cases.getAllCasesMetrics({ from: 'now-1d' })).toEqual(casesStatus); + expect(await api.cases.getCasesMetrics({ features: ['mttr'], from: 'now-1d' })).toEqual({ + mttr: 0, + }); }); it('should have been called with the correct path', async () => { - await api.cases.getAllCasesMetrics({ from: 'now-1d' }); - expect(http.get).toHaveBeenCalledWith('/api/cases/status', { - query: { from: 'now-1d' }, + await api.cases.getCasesMetrics({ features: ['mttr'], from: 'now-1d' }); + expect(http.get).toHaveBeenCalledWith('/api/cases/metrics', { + query: { features: ['mttr'], from: 'now-1d' }, }); }); }); diff --git a/x-pack/plugins/cases/public/client/api/index.ts b/x-pack/plugins/cases/public/client/api/index.ts index 8f55f7a4f0739..e9caee514a9cd 100644 --- a/x-pack/plugins/cases/public/client/api/index.ts +++ b/x-pack/plugins/cases/public/client/api/index.ts @@ -11,11 +11,11 @@ import { CasesByAlertIDRequest, CasesFindRequest, getCasesFromAlertsUrl, - CasesResponse, CasesStatusRequest, - CasesStatusResponse, + CasesMetricsRequest, } from '../../../common/api'; -import { CASE_FIND_URL, CASE_STATUS_URL } from '../../../common/constants'; +import { Cases, CasesStatus, CasesMetrics } from '../../../common/ui'; +import { getCases, getCasesMetrics, getCasesStatus } from '../../api'; import { CasesUiStart } from '../../types'; export const createClientAPI = ({ http }: { http: HttpStart }): CasesUiStart['api'] => { @@ -26,10 +26,12 @@ export const createClientAPI = ({ http }: { http: HttpStart }): CasesUiStart['ap ): Promise => http.get(getCasesFromAlertsUrl(alertId), { query }), cases: { - find: (query: CasesFindRequest): Promise => - http.get(CASE_FIND_URL, { query }), - getAllCasesMetrics: (query: CasesStatusRequest): Promise => - http.get(CASE_STATUS_URL, { query }), + find: (query: CasesFindRequest, signal?: AbortSignal): Promise => + getCases({ http, query, signal }), + getCasesStatus: (query: CasesStatusRequest, signal?: AbortSignal): Promise => + getCasesStatus({ http, query, signal }), + getCasesMetrics: (query: CasesMetricsRequest, signal?: AbortSignal): Promise => + getCasesMetrics({ http, signal, query }), }, }; }; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts index 02a099f9c6a1f..a53a1e9ea452f 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts @@ -12,12 +12,12 @@ import { i18n } from '@kbn/i18n'; import { AuthenticatedUser } from '@kbn/security-plugin/common/model'; import { NavigateToAppOptions } from '@kbn/core/public'; +import { convertToCamelCase } from '../../../api/utils'; import { FEATURE_ID, DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ, } from '../../../../common/constants'; -import { convertToCamelCase } from '../../../containers/utils'; import { StartServices } from '../../../types'; import { useUiSetting, useKibana } from './kibana_react'; diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index 10005b2c87bce..dbc57e163d3ff 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -198,6 +198,10 @@ export const MARKED_CASE_AS = i18n.translate('xpack.cases.caseView.markedCaseAs' defaultMessage: 'marked case as', }); +export const SET_SEVERITY_TO = i18n.translate('xpack.cases.caseView.setSeverityTo', { + defaultMessage: 'set severity to', +}); + export const OPEN_CASES = i18n.translate('xpack.cases.caseTable.openCases', { defaultMessage: 'Open cases', }); diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index 87d53aae14e28..f0a3502fd6813 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -17,7 +17,7 @@ import { TestProviders } from '../../common/mock'; import { casesStatus, useGetCasesMockState, mockCase, connectorsMock } from '../../containers/mock'; import { StatusAll } from '../../../common/ui/types'; -import { CaseStatuses } from '../../../common/api'; +import { CaseSeverity, CaseStatuses } from '../../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { getEmptyTagValue } from '../empty_value'; import { useDeleteCases } from '../../containers/use_delete_cases'; @@ -560,6 +560,7 @@ describe('AllCasesListGeneric', () => { username: 'lknope', }, description: 'Security banana Issue', + severity: CaseSeverity.LOW, duration: null, externalService: { connectorId: '123', diff --git a/x-pack/plugins/cases/public/components/all_cases/table.tsx b/x-pack/plugins/cases/public/components/all_cases/table.tsx index 8190acce9e784..ce3442e734b43 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table.tsx @@ -19,13 +19,13 @@ import styled from 'styled-components'; import { CasesTableUtilityBar } from './utility_bar'; import { LinkButton } from '../links'; -import { AllCases, Case, FilterOptions } from '../../../common/ui/types'; +import { Cases, Case, FilterOptions } from '../../../common/ui/types'; import * as i18n from './translations'; import { useCreateCaseNavigation } from '../../common/navigation'; interface CasesTableProps { columns: EuiBasicTableProps['columns']; - data: AllCases; + data: Cases; filterOptions: FilterOptions; goToCreateCase?: () => void; handleIsLoading: (a: boolean) => void; diff --git a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx index 64e22d7aa4e9c..a6247cedd3fac 100644 --- a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx @@ -15,14 +15,14 @@ import { UtilityBarText, } from '../utility_bar'; import * as i18n from './translations'; -import { AllCases, Case, DeleteCase, FilterOptions } from '../../../common/ui/types'; +import { Cases, Case, DeleteCase, FilterOptions } from '../../../common/ui/types'; import { getBulkItems } from '../bulk_actions'; import { useDeleteCases } from '../../containers/use_delete_cases'; import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; import { useUpdateCases } from '../../containers/use_bulk_update_case'; interface OwnProps { - data: AllCases; + data: Cases; enableBulkActions: boolean; filterOptions: FilterOptions; handleIsLoading: (a: boolean) => void; diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx index b9e4beb5d7e26..452601d142848 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.tsx @@ -7,6 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; +import { CaseSeverity } from '../../../../common/api'; import { useConnectors } from '../../../containers/configure/use_connectors'; import { useCaseViewNavigation } from '../../../common/navigation'; import { UpdateKey, UseFetchAlertData } from '../../../../common/ui/types'; @@ -23,6 +24,7 @@ import * as i18n from '../translations'; import { getNoneConnector, normalizeActionConnector } from '../../configure_cases/utils'; import { getConnectorById } from '../../utils'; import { UseGetCaseUserActions } from '../../../containers/use_get_case_user_actions'; +import { SeveritySidebarSelector } from '../../severity/sidebar_selector'; export const CaseViewActivity = ({ initLoadingData, @@ -108,6 +110,12 @@ export const CaseViewActivity = ({ (newTags) => onUpdateField({ key: 'tags', value: newTags }), [onUpdateField] ); + + const onUpdateSeverity = useCallback( + (newSeverity: CaseSeverity) => onUpdateField({ key: 'severity', value: newSeverity }), + [onUpdateField] + ); + const { loading: isLoadingConnectors, connectors } = useConnectors(); const [connectorName, isValidConnector] = useMemo(() => { @@ -180,6 +188,12 @@ export const CaseViewActivity = ({ )} + (value); + if (caseData.severity !== value) { + callUpdate('severity', severityUpdate); + } default: return null; } diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index ac2729564b387..50a3c69f2073e 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -35,6 +35,7 @@ import { CreateCaseOwnerSelector } from './owner_selector'; import { useCasesContext } from '../cases_context/use_cases_context'; import { useAvailableCasesOwners } from '../app/use_available_owners'; import { CaseAttachments } from '../../types'; +import { Severity } from './severity'; interface ContainerProps { big?: boolean; @@ -88,6 +89,9 @@ export const CreateCaseFormFields: React.FC = React.m + + + {canShowCaseSolutionSelection && ( = React.m + ), }), diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index 634f518ae5ebd..bfa4f391458da 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -10,7 +10,7 @@ import { mount, ReactWrapper } from 'enzyme'; import { act, RenderResult, waitFor, within } from '@testing-library/react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { CommentType, ConnectorTypes } from '../../../common/api'; +import { CaseSeverity, CommentType, ConnectorTypes } from '../../../common/api'; import { useKibana } from '../../common/lib/kibana'; import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock'; import { usePostCase } from '../../containers/use_post_case'; @@ -182,6 +182,7 @@ describe('Create case', () => { ); expect(renderResult.getByTestId('caseTitle')).toBeTruthy(); + expect(renderResult.getByTestId('caseSeverity')).toBeTruthy(); expect(renderResult.getByTestId('caseDescription')).toBeTruthy(); expect(renderResult.getByTestId('caseTags')).toBeTruthy(); expect(renderResult.getByTestId('caseConnectors')).toBeTruthy(); @@ -208,6 +209,34 @@ describe('Create case', () => { }); }); + it('should post a case on submit click with the selected severity', async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const renderResult = mockedContext.render( + + + + + ); + + await fillFormReactTestingLib(renderResult); + + userEvent.click(renderResult.getByTestId('case-severity-selection')); + expect(renderResult.getByTestId('case-severity-selection-high')).toBeTruthy(); + userEvent.click(renderResult.getByTestId('case-severity-selection-high')); + + userEvent.click(renderResult.getByTestId('create-case-submit')); + await waitFor(() => { + expect(postCase).toBeCalledWith({ + ...sampleData, + severity: CaseSeverity.HIGH, + }); + }); + }); + it('does not submits the title when the length is longer than 64 characters', async () => { const longTitle = 'This is a title that should not be saved as it is longer than 64 characters.'; @@ -285,6 +314,18 @@ describe('Create case', () => { ); }); + it('should select LOW as the default severity', async () => { + const renderResult = mockedContext.render( + + + + + ); + expect(renderResult.getByTestId('caseSeverity')).toBeTruthy(); + // there should be 2 low elements. one for the options popover and one for the displayed one. + expect(renderResult.getAllByTestId('case-severity-selection-low').length).toBe(2); + }); + it('should select the default connector set in the configuration', async () => { useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index 4385053a8c8c0..a65e9f5960e9d 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -14,7 +14,7 @@ import { usePostPushToService } from '../../containers/use_post_push_to_service' import { useConnectors } from '../../containers/configure/use_connectors'; import { Case } from '../../containers/types'; -import { NONE_CONNECTOR_ID } from '../../../common/api'; +import { CaseSeverity, NONE_CONNECTOR_ID } from '../../../common/api'; import { UseCreateAttachments, useCreateAttachments, @@ -28,6 +28,7 @@ const initialCaseValue: FormProps = { description: '', tags: [], title: '', + severity: CaseSeverity.LOW, connectorId: NONE_CONNECTOR_ID, fields: null, syncAlerts: true, diff --git a/x-pack/plugins/cases/public/components/create/mock.ts b/x-pack/plugins/cases/public/components/create/mock.ts index 8ab515c79f67e..38d57bf24781e 100644 --- a/x-pack/plugins/cases/public/components/create/mock.ts +++ b/x-pack/plugins/cases/public/components/create/mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CasePostRequest, ConnectorTypes } from '../../../common/api'; +import { CasePostRequest, CaseSeverity, ConnectorTypes } from '../../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { choices } from '../connectors/mock'; @@ -13,6 +13,7 @@ export const sampleTags = ['coke', 'pepsi']; export const sampleData: CasePostRequest = { description: 'what a great description', tags: sampleTags, + severity: CaseSeverity.LOW, title: 'what a cool title', connector: { fields: null, diff --git a/x-pack/plugins/cases/public/components/create/schema.tsx b/x-pack/plugins/cases/public/components/create/schema.tsx index b7c363b263998..d72b1cc523f0d 100644 --- a/x-pack/plugins/cases/public/components/create/schema.tsx +++ b/x-pack/plugins/cases/public/components/create/schema.tsx @@ -17,6 +17,7 @@ import { import * as i18n from './translations'; import { OptionalFieldLabel } from './optional_field_label'; +import { SEVERITY_TITLE } from '../severity/translations'; const { emptyField, maxLengthField } = fieldValidators; export const schemaTags = { @@ -83,6 +84,9 @@ export const schema: FormSchema = { ], }, tags: schemaTags, + severity: { + label: SEVERITY_TITLE, + }, connectorId: { type: FIELD_TYPES.SUPER_SELECT, label: i18n.CONNECTORS, diff --git a/x-pack/plugins/cases/public/components/create/severity.test.tsx b/x-pack/plugins/cases/public/components/create/severity.test.tsx new file mode 100644 index 0000000000000..d2434a37a4392 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/severity.test.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 { CaseSeverity } from '../../../common/api'; +import React from 'react'; +import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; +import { Form, FormHook, useForm } from '../../common/shared_imports'; +import { Severity } from './severity'; +import { FormProps, schema } from './schema'; +import userEvent from '@testing-library/user-event'; +import { waitFor } from '@testing-library/dom'; + +let globalForm: FormHook; +const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm({ + defaultValue: { severity: CaseSeverity.LOW }, + schema: { + severity: schema.severity, + }, + }); + + globalForm = form; + + return {children}; +}; +describe('Severity form field', () => { + let appMockRender: AppMockRenderer; + beforeEach(() => { + appMockRender = createAppMockRenderer(); + }); + it('renders', () => { + const result = appMockRender.render( + + + + ); + expect(result.getByTestId('caseSeverity')).toBeTruthy(); + expect(result.getByTestId('case-severity-selection')).not.toHaveAttribute('disabled'); + }); + + // default to LOW in this test configuration + it('defaults to the correct value', () => { + const result = appMockRender.render( + + + + ); + expect(result.getByTestId('caseSeverity')).toBeTruthy(); + // two items. one for the popover one for the selected field + expect(result.getAllByTestId('case-severity-selection-low').length).toBe(2); + }); + + it('selects the correct value when changed', async () => { + const result = appMockRender.render( + + + + ); + expect(result.getByTestId('caseSeverity')).toBeTruthy(); + userEvent.click(result.getByTestId('case-severity-selection')); + userEvent.click(result.getByTestId('case-severity-selection-high')); + await waitFor(() => { + expect(globalForm.getFormData()).toEqual({ severity: 'high' }); + }); + }); + + it('disables when loading data', () => { + const result = appMockRender.render( + + + + ); + expect(result.getByTestId('case-severity-selection')).toHaveAttribute('disabled'); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/severity.tsx b/x-pack/plugins/cases/public/components/create/severity.tsx new file mode 100644 index 0000000000000..730eab5d77ac6 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/severity.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFormRow } from '@elastic/eui'; +import React, { memo } from 'react'; +import { CaseSeverity } from '../../../common/api'; +import { UseField, useFormContext, useFormData } from '../../common/shared_imports'; +import { SeveritySelector } from '../severity/selector'; +import { SEVERITY_TITLE } from '../severity/translations'; + +interface Props { + isLoading: boolean; +} + +const SeverityFieldFormComponent = ({ isLoading }: { isLoading: boolean }) => { + const { setFieldValue } = useFormContext(); + const [{ severity }] = useFormData({ watch: ['severity'] }); + const onSeverityChange = (newSeverity: CaseSeverity) => { + setFieldValue('severity', newSeverity); + }; + return ( + + + + ); +}; +SeverityFieldFormComponent.displayName = 'SeverityFieldForm'; + +const SeverityComponent: React.FC = ({ isLoading }) => ( + +); + +SeverityComponent.displayName = 'SeverityComponent'; + +export const Severity = memo(SeverityComponent); diff --git a/x-pack/plugins/cases/public/components/severity/config.ts b/x-pack/plugins/cases/public/components/severity/config.ts new file mode 100644 index 0000000000000..945eb94640d3c --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/config.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { euiLightVars } from '@kbn/ui-theme'; +import { CaseSeverity } from '../../../common/api'; +import { CRITICAL, HIGH, LOW, MEDIUM } from './translations'; + +export const severities = { + [CaseSeverity.LOW]: { + color: euiLightVars.euiColorVis0, + label: LOW, + }, + [CaseSeverity.MEDIUM]: { + color: euiLightVars.euiColorVis5, + label: MEDIUM, + }, + [CaseSeverity.HIGH]: { + color: euiLightVars.euiColorVis7, + label: HIGH, + }, + [CaseSeverity.CRITICAL]: { + color: euiLightVars.euiColorVis9, + label: CRITICAL, + }, +}; diff --git a/x-pack/plugins/cases/public/components/severity/selector.test.tsx b/x-pack/plugins/cases/public/components/severity/selector.test.tsx new file mode 100644 index 0000000000000..126dc64e7af1b --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/selector.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseSeverity } from '../../../common/api'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { SeveritySelector } from './selector'; +import userEvent from '@testing-library/user-event'; + +describe('Severity field selector', () => { + const onSeverityChange = jest.fn(); + it('renders a list of severity fields', () => { + const result = render( + + ); + + expect(result.getByTestId('case-severity-selection')).toBeTruthy(); + expect(result.getAllByTestId('case-severity-selection-medium').length).toBeTruthy(); + }); + + it('renders a list of severity options when clicked', () => { + const result = render( + + ); + userEvent.click(result.getByTestId('case-severity-selection')); + expect(result.getByTestId('case-severity-selection-low')).toBeTruthy(); + expect(result.getAllByTestId('case-severity-selection-medium').length).toBeTruthy(); + expect(result.getByTestId('case-severity-selection-high')).toBeTruthy(); + expect(result.getByTestId('case-severity-selection-critical')).toBeTruthy(); + }); + + it('calls onSeverityChange with the newly selected severity when clicked', () => { + const result = render( + + ); + userEvent.click(result.getByTestId('case-severity-selection')); + expect(result.getByTestId('case-severity-selection-low')).toBeTruthy(); + userEvent.click(result.getByTestId('case-severity-selection-low')); + expect(onSeverityChange).toHaveBeenLastCalledWith('low'); + }); +}); diff --git a/x-pack/plugins/cases/public/components/severity/selector.tsx b/x-pack/plugins/cases/public/components/severity/selector.tsx new file mode 100644 index 0000000000000..0d1ff4b319f2b --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/selector.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 { + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiSuperSelect, + EuiSuperSelectOption, +} from '@elastic/eui'; +import React from 'react'; +import { CaseSeverity } from '../../../common/api'; +import { severities } from './config'; + +interface Props { + selectedSeverity: CaseSeverity; + onSeverityChange: (status: CaseSeverity) => void; + isLoading: boolean; + isDisabled: boolean; +} + +export const SeveritySelector: React.FC = ({ + selectedSeverity, + onSeverityChange, + isLoading, + isDisabled, +}) => { + const caseSeverities = Object.keys(severities) as CaseSeverity[]; + const options: Array> = caseSeverities.map((severity) => { + const severityData = severities[severity]; + return { + value: severity, + inputDisplay: ( + + + {severityData.label} + + + ), + }; + }); + + return ( + + ); +}; +SeveritySelector.displayName = 'SeveritySelector'; diff --git a/x-pack/plugins/cases/public/components/severity/sidebar_selector.tsx b/x-pack/plugins/cases/public/components/severity/sidebar_selector.tsx new file mode 100644 index 0000000000000..ff591e342793f --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/sidebar_selector.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 { EuiFlexItem, EuiHorizontalRule, EuiSpacer, EuiText } from '@elastic/eui'; +import React from 'react'; +import { CaseSeverity } from '../../../common/api'; +import { SeveritySelector } from './selector'; +import { SEVERITY_TITLE } from './translations'; + +interface Props { + selectedSeverity: CaseSeverity; + onSeverityChange: (status: CaseSeverity) => void; + isLoading: boolean; + isDisabled: boolean; +} + +export const SeveritySidebarSelector: React.FC = ({ + selectedSeverity, + onSeverityChange, + isLoading, + isDisabled, +}) => { + return ( + + +

{SEVERITY_TITLE}

+
+ + + +
+ ); +}; +SeveritySidebarSelector.displayName = 'SeveritySidebarSelector'; diff --git a/x-pack/plugins/cases/public/components/severity/translations.ts b/x-pack/plugins/cases/public/components/severity/translations.ts new file mode 100644 index 0000000000000..b5982c70ed690 --- /dev/null +++ b/x-pack/plugins/cases/public/components/severity/translations.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const LOW = i18n.translate('xpack.cases.severity.low', { + defaultMessage: 'Low', +}); + +export const MEDIUM = i18n.translate('xpack.cases.severity.medium', { + defaultMessage: 'Medium', +}); + +export const HIGH = i18n.translate('xpack.cases.severity.high', { + defaultMessage: 'High', +}); + +export const CRITICAL = i18n.translate('xpack.cases.severity.critical', { + defaultMessage: 'Critical', +}); + +export const SEVERITY_TITLE = i18n.translate('xpack.cases.severity.title', { + defaultMessage: 'Severity', +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/builder.tsx b/x-pack/plugins/cases/public/components/user_actions/builder.tsx index 5e1c11fbdd2df..36298bbae601b 100644 --- a/x-pack/plugins/cases/public/components/user_actions/builder.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/builder.tsx @@ -10,6 +10,7 @@ import { createConnectorUserActionBuilder } from './connector'; import { createDescriptionUserActionBuilder } from './description'; import { createPushedUserActionBuilder } from './pushed'; import { createSettingsUserActionBuilder } from './settings'; +import { createSeverityUserActionBuilder } from './severity'; import { createStatusUserActionBuilder } from './status'; import { createTagsUserActionBuilder } from './tags'; import { createTitleUserActionBuilder } from './title'; @@ -20,6 +21,7 @@ export const builderMap: UserActionBuilderMap = { tags: createTagsUserActionBuilder, title: createTitleUserActionBuilder, status: createStatusUserActionBuilder, + severity: createSeverityUserActionBuilder, pushed: createPushedUserActionBuilder, comment: createCommentUserActionBuilder, description: createDescriptionUserActionBuilder, diff --git a/x-pack/plugins/cases/public/components/user_actions/severity.test.tsx b/x-pack/plugins/cases/public/components/user_actions/severity.test.tsx new file mode 100644 index 0000000000000..d92a5cb5a153d --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/severity.test.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 { EuiCommentList } from '@elastic/eui'; +import { Actions, CaseSeverity } from '../../../common/api'; +import React from 'react'; +import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; +import { getUserAction } from '../../containers/mock'; +import { getMockBuilderArgs } from './mock'; +import { createSeverityUserActionBuilder } from './severity'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../common/navigation/hooks'); + +const builderArgs = getMockBuilderArgs(); +describe('createSeverityUserActionBuilder', () => { + let appMockRenderer: AppMockRenderer; + beforeEach(() => { + appMockRenderer = createAppMockRenderer(); + }); + it('renders correctly', () => { + const userAction = getUserAction('severity', Actions.update, { + payload: { severity: CaseSeverity.LOW }, + }); + const builder = createSeverityUserActionBuilder({ + ...builderArgs, + userAction, + }); + const createdUserAction = builder.build(); + + const result = appMockRenderer.render(); + expect(result.getByTestId('severity-update-user-action-severity-title')).toBeTruthy(); + expect(result.getByTestId('severity-update-user-action-severity-title-low')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/severity.tsx b/x-pack/plugins/cases/public/components/user_actions/severity.tsx new file mode 100644 index 0000000000000..3e2cf8605b080 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/severity.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiHealth } from '@elastic/eui'; +import React from 'react'; +import { SeverityUserAction } from '../../../common/api/cases/user_actions/severity'; +import { SET_SEVERITY_TO } from '../create/translations'; +import { createCommonUpdateUserActionBuilder } from './common'; +import { UserActionBuilder, UserActionResponse } from './types'; +import { severities } from '../severity/config'; + +const getLabelTitle = (userAction: UserActionResponse) => { + const severity = userAction.payload.severity; + const severityData = severities[severity]; + if (severityData === undefined) { + return null; + } + return ( + + {SET_SEVERITY_TO} + + {severityData.label} + + + ); +}; + +export const createSeverityUserActionBuilder: UserActionBuilder = ({ + userAction, + handleOutlineComment, +}) => ({ + build: () => { + const severityUserAction = userAction as UserActionResponse; + const label = getLabelTitle(severityUserAction); + const commonBuilder = createCommonUpdateUserActionBuilder({ + userAction, + handleOutlineComment, + label, + icon: 'dot', + }); + + return commonBuilder.build(); + }, +}); diff --git a/x-pack/plugins/cases/public/containers/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/__mocks__/api.ts index 3906997349357..4b2029a83d6dd 100644 --- a/x-pack/plugins/cases/public/containers/__mocks__/api.ts +++ b/x-pack/plugins/cases/public/containers/__mocks__/api.ts @@ -7,7 +7,7 @@ import { ActionLicense, - AllCases, + Cases, BulkUpdateStatus, Case, CasesStatus, @@ -84,7 +84,7 @@ export const getCases = async ({ sortOrder: 'desc', }, signal, -}: FetchCasesProps): Promise => Promise.resolve(allCases); +}: FetchCasesProps): Promise => Promise.resolve(allCases); export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): Promise => Promise.resolve(basicCasePost); diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index 6b8bd6f65bc6d..0b996ec1c7a07 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { httpServiceMock } from '@kbn/core/public/mocks'; import { KibanaServices } from '../common/lib/kibana'; import { ConnectorTypes, CommentType, CaseStatuses } from '../../common/api'; @@ -20,7 +21,6 @@ import { getActionLicense, getCase, getCases, - getCasesStatus, getCaseUserActions, getReporters, getTags, @@ -54,6 +54,7 @@ import { } from './mock'; import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; +import { getCasesStatus } from '../api'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; @@ -246,21 +247,33 @@ describe('Case Configuration API', () => { }); describe('getCasesStatus', () => { + const http = httpServiceMock.createStartContract({ basePath: '' }); + http.get.mockResolvedValue(casesStatusSnake); + beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(casesStatusSnake); }); + 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', + await getCasesStatus({ + http, + signal: abortCtrl.signal, + query: { owner: [SECURITY_SOLUTION_OWNER] }, + }); + + expect(http.get).toHaveBeenCalledWith(`${CASES_URL}/status`, { signal: abortCtrl.signal, query: { owner: [SECURITY_SOLUTION_OWNER] }, }); }); test('should return correct response', async () => { - const resp = await getCasesStatus(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); + const resp = await getCasesStatus({ + http, + signal: abortCtrl.signal, + query: { owner: SECURITY_SOLUTION_OWNER }, + }); + expect(resp).toEqual(casesStatus); }); }); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index a33f0e2501ac0..63a2ea794e065 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -6,17 +6,20 @@ */ import { omit } from 'lodash'; - -import { StatusAll, ResolvedCase } from '../../common/ui/types'; +import { + Cases, + FetchCasesProps, + ResolvedCase, + SortFieldCase, + StatusAll, +} from '../../common/ui/types'; import { BulkCreateCommentRequest, CasePatchRequest, CasePostRequest, CaseResponse, CaseResolveResponse, - CasesFindResponse, CasesResponse, - CasesStatusResponse, CaseUserActionsResponse, CommentRequest, CommentType, @@ -28,10 +31,10 @@ import { User, getCaseCommentDeleteUrl, SingleCaseMetricsResponse, + CasesFindResponse, } from '../../common/api'; import { CASE_REPORTERS_URL, - CASE_STATUS_URL, CASE_TAGS_URL, CASES_URL, INTERNAL_BULK_CREATE_ATTACHMENTS_URL, @@ -40,31 +43,25 @@ import { getAllConnectorTypesUrl } from '../../common/utils/connectors_api'; import { KibanaServices } from '../common/lib/kibana'; +import { convertAllCasesToCamel, convertToCamelCase, convertArrayToCamelCase } from '../api/utils'; + import { ActionLicense, - AllCases, BulkUpdateStatus, Case, SingleCaseMetrics, SingleCaseMetricsFeature, - CasesStatus, - FetchCasesProps, - SortFieldCase, CaseUserActions, } from './types'; import { - convertToCamelCase, - convertAllCasesToCamel, - convertArrayToCamelCase, decodeCaseResponse, decodeCasesResponse, - decodeCasesFindResponse, - decodeCasesStatusResponse, decodeCaseUserActionsResponse, decodeCaseResolveResponse, decodeSingleCaseMetricsResponse, } from './utils'; +import { decodeCasesFindResponse } from '../api/decoders'; export const getCase = async ( caseId: string, @@ -99,18 +96,6 @@ export const resolveCase = async ( return convertToCamelCase(decodeCaseResolveResponse(response)); }; -export const getCasesStatus = async ( - signal: AbortSignal, - owner: string[] -): Promise => { - const response = await KibanaServices.get().http.fetch(CASE_STATUS_URL, { - method: 'GET', - signal, - query: { ...(owner.length > 0 ? { owner } : {}) }, - }); - return convertToCamelCase(decodeCasesStatusResponse(response)); -}; - export const getTags = async (signal: AbortSignal, owner: string[]): Promise => { const response = await KibanaServices.get().http.fetch(CASE_TAGS_URL, { method: 'GET', @@ -176,7 +161,7 @@ export const getCases = async ({ sortOrder: 'desc', }, signal, -}: FetchCasesProps): Promise => { +}: FetchCasesProps): Promise => { const query = { reporters: filterOptions.reporters.map((r) => r.username ?? '').filter((r) => r !== ''), tags: filterOptions.tags, @@ -185,11 +170,13 @@ export const getCases = async ({ ...(filterOptions.owner.length > 0 ? { owner: filterOptions.owner } : {}), ...queryParams, }; + const response = await KibanaServices.get().http.fetch(`${CASES_URL}/_find`, { method: 'GET', query: query.status === StatusAll ? omit(query, ['status']) : query, signal, }); + return convertAllCasesToCamel(decodeCasesFindResponse(response)); }; diff --git a/x-pack/plugins/cases/public/containers/configure/api.ts b/x-pack/plugins/cases/public/containers/configure/api.ts index 32202afc34881..53e045882cb7c 100644 --- a/x-pack/plugins/cases/public/containers/configure/api.ts +++ b/x-pack/plugins/cases/public/containers/configure/api.ts @@ -18,14 +18,9 @@ import { } from '../../../common/api'; import { CASE_CONFIGURE_CONNECTORS_URL, CASE_CONFIGURE_URL } from '../../../common/constants'; import { KibanaServices } from '../../common/lib/kibana'; - +import { convertToCamelCase, convertArrayToCamelCase } from '../../api/utils'; import { ApiProps } from '../types'; -import { - convertArrayToCamelCase, - convertToCamelCase, - decodeCaseConfigurationsResponse, - decodeCaseConfigureResponse, -} from '../utils'; +import { decodeCaseConfigurationsResponse, decodeCaseConfigureResponse } from '../utils'; import { CaseConfigure } from './types'; export const fetchConnectors = async ({ signal }: ApiProps): Promise => { diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index 8c45fd5b083e0..ed9e9ebd1ff8f 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment } from './types'; +import { ActionLicense, Cases, Case, CasesStatus, CaseUserActions, Comment } from './types'; import type { ResolvedCase, @@ -31,6 +31,7 @@ import { UserActionTypes, UserActionWithResponse, CommentUserAction, + CaseSeverity, } from '../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { UseGetCasesState, DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; @@ -154,6 +155,7 @@ export const basicCase: Case = { fields: null, }, description: 'Security banana Issue', + severity: CaseSeverity.LOW, duration: null, externalService: null, status: CaseStatuses.open, @@ -247,6 +249,7 @@ export const mockCase: Case = { fields: null, }, duration: null, + severity: CaseSeverity.LOW, description: 'Security banana Issue', externalService: null, status: CaseStatuses.open, @@ -330,7 +333,7 @@ export const cases: Case[] = [ caseWithAlertsSyncOff, ]; -export const allCases: AllCases = { +export const allCases: Cases = { cases, page: 1, perPage: 5, @@ -512,6 +515,7 @@ export const getUserAction = ( description: 'a desc', connector: { ...getJiraConnector() }, status: CaseStatuses.open, + severity: CaseSeverity.LOW, title: 'a title', tags: ['a tag'], settings: { syncAlerts: true }, diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.tsx index 283cdfdf39aa4..d817dc9d9ac0f 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases.tsx @@ -8,7 +8,7 @@ import { useCallback, useEffect, useReducer, useRef } from 'react'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants'; import { - AllCases, + Cases, Case, FilterOptions, QueryParams, @@ -21,7 +21,7 @@ import * as i18n from './translations'; import { getCases, patchCase } from './api'; export interface UseGetCasesState { - data: AllCases; + data: Cases; filterOptions: FilterOptions; isError: boolean; loading: string[]; @@ -39,7 +39,7 @@ export type Action = | { type: 'FETCH_INIT'; payload: string } | { type: 'FETCH_CASES_SUCCESS'; - payload: AllCases; + payload: Cases; } | { type: 'FETCH_FAILURE'; payload: string } | { type: 'FETCH_UPDATE_CASE_SUCCESS' } @@ -114,11 +114,11 @@ export const DEFAULT_QUERY_PARAMS: QueryParams = { sortOrder: 'desc', }; -export const initialData: AllCases = { +export const initialData: Cases = { cases: [], - countClosedCases: null, - countInProgressCases: null, - countOpenCases: null, + countClosedCases: 0, + countInProgressCases: 0, + countOpenCases: 0, page: 0, perPage: 0, total: 0, diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx index 2e3e42255145d..3978e944db949 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { useGetCasesStatus, UseGetCasesStatus } from './use_get_cases_status'; import { casesStatus } from './mock'; -import * as api from './api'; +import * as api from '../api'; import { TestProviders } from '../common/mock'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; -jest.mock('./api'); +jest.mock('../api'); jest.mock('../common/lib/kibana'); describe('useGetCasesStatus', () => { @@ -30,9 +30,9 @@ describe('useGetCasesStatus', () => { await act(async () => { expect(result.current).toEqual({ - countClosedCases: null, - countOpenCases: null, - countInProgressCases: null, + countClosedCases: 0, + countOpenCases: 0, + countInProgressCases: 0, isLoading: true, isError: false, fetchCasesStatus: result.current.fetchCasesStatus, @@ -49,12 +49,17 @@ describe('useGetCasesStatus', () => { wrapper: ({ children }) => {children}, } ); + await waitForNextUpdate(); - expect(spyOnGetCasesStatus).toBeCalledWith(abortCtrl.signal, [SECURITY_SOLUTION_OWNER]); + expect(spyOnGetCasesStatus).toBeCalledWith({ + http: expect.anything(), + signal: abortCtrl.signal, + query: { owner: [SECURITY_SOLUTION_OWNER] }, + }); }); }); - it('fetch reporters', async () => { + it('fetch statuses', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => useGetCasesStatus(), @@ -62,6 +67,7 @@ describe('useGetCasesStatus', () => { wrapper: ({ children }) => {children}, } ); + await waitForNextUpdate(); expect(result.current).toEqual({ countClosedCases: casesStatus.countClosedCases, diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx index 5e21c339856fb..6530236a2fee6 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases_status.tsx @@ -8,10 +8,10 @@ import { useCallback, useEffect, useState, useRef } from 'react'; import { useCasesContext } from '../components/cases_context/use_cases_context'; -import { getCasesStatus } from './api'; import * as i18n from './translations'; import { CasesStatus } from './types'; -import { useToasts } from '../common/lib/kibana'; +import { useHttp, useToasts } from '../common/lib/kibana'; +import { getCasesStatus } from '../api'; interface CasesStatusState extends CasesStatus { isLoading: boolean; @@ -19,9 +19,9 @@ interface CasesStatusState extends CasesStatus { } const initialData: CasesStatusState = { - countClosedCases: null, - countInProgressCases: null, - countOpenCases: null, + countClosedCases: 0, + countInProgressCases: 0, + countOpenCases: 0, isLoading: true, isError: false, }; @@ -31,6 +31,7 @@ export interface UseGetCasesStatus extends CasesStatusState { } export const useGetCasesStatus = (): UseGetCasesStatus => { + const http = useHttp(); const { owner } = useCasesContext(); const [casesStatusState, setCasesStatusState] = useState(initialData); const toasts = useToasts(); @@ -47,7 +48,11 @@ export const useGetCasesStatus = (): UseGetCasesStatus => { isLoading: true, }); - const response = await getCasesStatus(abortCtrlRef.current.signal, owner); + const response = await getCasesStatus({ + http, + signal: abortCtrlRef.current.signal, + query: { owner }, + }); if (!isCancelledRef.current) { setCasesStatusState({ diff --git a/x-pack/plugins/cases/public/containers/utils.ts b/x-pack/plugins/cases/public/containers/utils.ts index deafcda2d24ea..a9a0eff53c07c 100644 --- a/x-pack/plugins/cases/public/containers/utils.ts +++ b/x-pack/plugins/cases/public/containers/utils.ts @@ -5,22 +5,17 @@ * 2.0. */ -import { set } from '@elastic/safer-lodash-set'; -import { camelCase, isArray, isObject, transform, snakeCase } from 'lodash'; +import { isObject, transform, snakeCase } from 'lodash'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { ToastInputFields } from '@kbn/core/public'; import { - CasesFindResponse, - CasesFindResponseRt, CaseResponse, CaseResponseRt, CasesResponse, CasesResponseRt, - CasesStatusResponseRt, - CasesStatusResponse, throwErrors, CasesConfigurationsResponse, CaseConfigurationsResponseRt, @@ -35,7 +30,7 @@ import { SingleCaseMetricsResponse, SingleCaseMetricsResponseRt, } from '../../common/api'; -import { AllCases, Case, UpdateByKey } from './types'; +import { Case, UpdateByKey } from './types'; import * as i18n from './translations'; export const getTypedPayload = (a: unknown): T => a as T; @@ -46,45 +41,6 @@ export const covertToSnakeCase = (obj: Record) => acc[camelKey] = isObject(value) ? covertToSnakeCase(value as Record) : value; }); -export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => - arrayOfSnakes.reduce((acc: unknown[], value) => { - if (isArray(value)) { - return [...acc, convertArrayToCamelCase(value)]; - } else if (isObject(value)) { - return [...acc, convertToCamelCase(value)]; - } else { - return [...acc, value]; - } - }, []); - -export const convertToCamelCase = (obj: T): U => - Object.entries(obj).reduce((acc, [key, value]) => { - if (isArray(value)) { - set(acc, camelCase(key), convertArrayToCamelCase(value)); - } else if (isObject(value)) { - set(acc, camelCase(key), convertToCamelCase(value)); - } else { - set(acc, camelCase(key), value); - } - return acc; - }, {} as U); - -export const convertAllCasesToCamel = (snakeCases: CasesFindResponse): AllCases => ({ - cases: snakeCases.cases.map((theCase) => convertToCamelCase(theCase)), - countOpenCases: snakeCases.count_open_cases, - countInProgressCases: snakeCases.count_in_progress_cases, - countClosedCases: snakeCases.count_closed_cases, - page: snakeCases.page, - perPage: snakeCases.per_page, - total: snakeCases.total, -}); - -export const decodeCasesStatusResponse = (respCase?: CasesStatusResponse) => - pipe( - CasesStatusResponseRt.decode(respCase), - fold(throwErrors(createToasterPlainError), identity) - ); - export const createToasterPlainError = (message: string) => new ToasterError([message]); export const decodeCaseResponse = (respCase?: CaseResponse) => @@ -105,9 +61,6 @@ export const decodeSingleCaseMetricsResponse = (respCase?: SingleCaseMetricsResp export const decodeCasesResponse = (respCase?: CasesResponse) => pipe(CasesResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); -export const decodeCasesFindResponse = (respCases?: CasesFindResponse) => - pipe(CasesFindResponseRt.decode(respCases), fold(throwErrors(createToasterPlainError), identity)); - export const decodeCaseConfigurationsResponse = (respCase?: CasesConfigurationsResponse) => { return pipe( CaseConfigurationsResponseRt.decode(respCase), diff --git a/x-pack/plugins/cases/public/mocks.ts b/x-pack/plugins/cases/public/mocks.ts index f8c0eaaaef7de..5ec7a10c88acc 100644 --- a/x-pack/plugins/cases/public/mocks.ts +++ b/x-pack/plugins/cases/public/mocks.ts @@ -10,7 +10,7 @@ import { CasesUiStart } from './types'; const apiMock: jest.Mocked = { getRelatedCases: jest.fn(), - cases: { find: jest.fn(), getAllCasesMetrics: jest.fn() }, + cases: { find: jest.fn(), getCasesMetrics: jest.fn(), getCasesStatus: jest.fn() }, }; const uiMock: jest.Mocked = { diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index 65cc1de25d345..bff5e5fd14700 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -21,9 +21,8 @@ import { CasesByAlertId, CasesByAlertIDRequest, CasesFindRequest, - CasesResponse, + CasesMetricsRequest, CasesStatusRequest, - CasesStatusResponse, CommentRequestAlertType, CommentRequestUserType, } from '../common/api'; @@ -36,6 +35,7 @@ import type { GetCasesProps } from './client/ui/get_cases'; import { GetAllCasesSelectorModalProps } from './client/ui/get_all_cases_selector_modal'; import { GetCreateCaseFlyoutProps } from './client/ui/get_create_case_flyout'; import { GetRecentCasesProps } from './client/ui/get_recent_cases'; +import { Cases, CasesStatus, CasesMetrics } from '../common/ui'; export interface CasesPluginSetup { security: SecurityPluginSetup; @@ -76,8 +76,9 @@ export interface CasesUiStart { api: { getRelatedCases: (alertId: string, query: CasesByAlertIDRequest) => Promise; cases: { - find: (query: CasesFindRequest) => Promise; - getAllCasesMetrics: (query: CasesStatusRequest) => Promise; + find: (query: CasesFindRequest, signal?: AbortSignal) => Promise; + getCasesStatus: (query: CasesStatusRequest, signal?: AbortSignal) => Promise; + getCasesMetrics: (query: CasesMetricsRequest, signal?: AbortSignal) => Promise; }; }; ui: { diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index ab9f6a4305800..714c8199d11a5 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -14,12 +14,13 @@ import { SavedObjectsUtils } from '@kbn/core/server'; import { throwErrors, - excess, CaseResponseRt, CaseResponse, CasePostRequest, ActionTypes, CasePostRequestRt, + excess, + CaseSeverity, } from '../../../common/api'; import { MAX_TITLE_LENGTH } from '../../../common/constants'; import { isInvalidTag } from '../../../common/utils/validators'; @@ -85,7 +86,7 @@ export const create = async ( unsecuredSavedObjectsClient, caseId: newCase.id, user, - payload: query, + payload: { ...query, severity: query.severity ?? CaseSeverity.LOW }, owner: newCase.attributes.owner, }); diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts index 69a5f2d3a587b..4c0698b209bef 100644 --- a/x-pack/plugins/cases/server/client/cases/mock.ts +++ b/x-pack/plugins/cases/server/client/cases/mock.ts @@ -240,6 +240,7 @@ export const userActions: CaseUserActionsResponse = [ }, settings: { syncAlerts: true }, status: 'open', + severity: 'low', owner: SECURITY_SOLUTION_OWNER, }, action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53', diff --git a/x-pack/plugins/cases/server/client/utils.test.ts b/x-pack/plugins/cases/server/client/utils.test.ts index 24e1135020a88..88140658c2b2b 100644 --- a/x-pack/plugins/cases/server/client/utils.test.ts +++ b/x-pack/plugins/cases/server/client/utils.test.ts @@ -5,9 +5,6 @@ * 2.0. */ -import { CaseConnector, ConnectorTypes } from '../../common/api'; -import { newCase } from '../routes/api/__mocks__/request_responses'; -import { transformNewCase } from '../common/utils'; import { buildRangeFilter, sortToSnake } from './utils'; import { toElasticsearchQuery } from '@kbn/es-query'; @@ -38,74 +35,6 @@ describe('utils', () => { }); }); - describe('transformNewCase', () => { - beforeAll(() => { - jest.useFakeTimers('modern'); - jest.setSystemTime(new Date('2020-04-09T09:43:51.778Z')); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - const connector: CaseConnector = { - id: '123', - name: 'My connector', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'High', parent: null }, - }; - it('transform correctly', () => { - const myCase = { - newCase: { ...newCase, connector }, - user: { - email: 'elastic@elastic.co', - full_name: 'Elastic', - username: 'elastic', - }, - }; - - const res = transformNewCase(myCase); - - expect(res).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "connector": Object { - "fields": Object { - "issueType": "Task", - "parent": null, - "priority": "High", - }, - "id": "123", - "name": "My connector", - "type": ".jira", - }, - "created_at": "2020-04-09T09:43:51.778Z", - "created_by": Object { - "email": "elastic@elastic.co", - "full_name": "Elastic", - "username": "elastic", - }, - "description": "A description", - "duration": null, - "external_service": null, - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "tags": Array [ - "new", - "case", - ], - "title": "My new case", - "updated_at": null, - "updated_by": null, - } - `); - }); - }); - describe('buildRangeFilter', () => { it('returns undefined if both the from and or are undefined', () => { const node = buildRangeFilter({}); diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 974c36bd0d8a6..918a48863cac0 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -9,11 +9,14 @@ import { SavedObject, SavedObjectsFindResponse } from '@kbn/core/server'; import { makeLensEmbeddableFactory } from '@kbn/lens-plugin/server/embeddable/make_lens_embeddable_factory'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { + CaseConnector, CaseResponse, + CaseSeverity, CommentAttributes, CommentRequest, CommentRequestUserType, CommentType, + ConnectorTypes, } from '../../common/api'; import { mockCaseComments, mockCases } from '../routes/api/__fixtures__/mock_saved_objects'; import { @@ -29,7 +32,9 @@ import { extractLensReferencesFromCommentString, getOrUpdateLensReferences, asArray, + transformNewCase, } from './utils'; +import { newCase } from '../routes/api/__mocks__/request_responses'; interface CommentReference { ids: string[]; @@ -67,6 +72,128 @@ function createCommentFindResponse( } describe('common utils', () => { + describe('transformNewCase', () => { + beforeAll(() => { + jest.useFakeTimers('modern'); + jest.setSystemTime(new Date('2020-04-09T09:43:51.778Z')); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + const connector: CaseConnector = { + id: '123', + name: 'My connector', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }; + + it('transform correctly', () => { + const myCase = { + newCase: { ...newCase, connector }, + user: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + }; + + const res = transformNewCase(myCase); + + expect(res).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "connector": Object { + "fields": Object { + "issueType": "Task", + "parent": null, + "priority": "High", + }, + "id": "123", + "name": "My connector", + "type": ".jira", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "elastic@elastic.co", + "full_name": "Elastic", + "username": "elastic", + }, + "description": "A description", + "duration": null, + "external_service": null, + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "severity": "low", + "status": "open", + "tags": Array [ + "new", + "case", + ], + "title": "My new case", + "updated_at": null, + "updated_by": null, + } + `); + }); + + it('transform correctly with severity provided', () => { + const myCase = { + newCase: { ...newCase, connector, severity: CaseSeverity.MEDIUM }, + user: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + }; + + const res = transformNewCase(myCase); + + expect(res).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "connector": Object { + "fields": Object { + "issueType": "Task", + "parent": null, + "priority": "High", + }, + "id": "123", + "name": "My connector", + "type": ".jira", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "elastic@elastic.co", + "full_name": "Elastic", + "username": "elastic", + }, + "description": "A description", + "duration": null, + "external_service": null, + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "severity": "medium", + "status": "open", + "tags": Array [ + "new", + "case", + ], + "title": "My new case", + "updated_at": null, + "updated_by": null, + } + `); + }); + }); + describe('transformCases', () => { it('transforms correctly', () => { const casesMap = new Map( @@ -110,6 +237,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "defacement", @@ -149,6 +277,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "Data Destruction", @@ -192,6 +321,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "LOLBins", @@ -239,6 +369,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "closed", "tags": Array [ "LOLBins", @@ -303,6 +434,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "LOLBins", @@ -358,6 +490,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "LOLBins", @@ -436,6 +569,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "LOLBins", @@ -489,6 +623,7 @@ describe('common utils', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "defacement", diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index 11e77c5eb4579..bc8dbf8a6e842 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -19,6 +19,7 @@ import { CaseAttributes, CasePostRequest, CaseResponse, + CaseSeverity, CasesFindResponse, CaseStatuses, CommentAttributes, @@ -56,6 +57,7 @@ export const transformNewCase = ({ }): CaseAttributes => ({ ...newCase, duration: null, + severity: newCase.severity ?? CaseSeverity.LOW, closed_at: null, closed_by: null, created_at: new Date().toISOString(), diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index cc45ef0e2d069..77e1a64012c6d 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -8,6 +8,7 @@ import { SavedObject } from '@kbn/core/server'; import { CaseAttributes, + CaseSeverity, CaseStatuses, CommentAttributes, CommentType, @@ -34,6 +35,7 @@ export const mockCases: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'This is a brand new case of a bad meanie defacing data', external_service: null, @@ -73,6 +75,7 @@ export const mockCases: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'Oh no, a bad meanie destroying data!', external_service: null, @@ -112,6 +115,7 @@ export const mockCases: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'Oh no, a bad meanie going LOLBins all over the place!', external_service: null, @@ -155,6 +159,7 @@ export const mockCases: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'Oh no, a bad meanie going LOLBins all over the place!', external_service: null, diff --git a/x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts b/x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts index 13bfa2093f623..394c0099b4991 100644 --- a/x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts +++ b/x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts @@ -19,17 +19,22 @@ export const getCaseMetricRoute = createCasesRoute({ case_id: schema.string({ minLength: 1 }), }), query: schema.object({ - features: schema.arrayOf(schema.string({ minLength: 1 })), + features: schema.oneOf([ + schema.arrayOf(schema.string({ minLength: 1 })), + schema.string({ minLength: 1 }), + ]), }), }, handler: async ({ context, request, response }) => { try { const caseContext = await context.cases; const client = await caseContext.getCasesClient(); + const { features } = request.query; + return response.ok({ body: await client.metrics.getCaseMetrics({ caseId: request.params.case_id, - features: request.query.features, + features: Array.isArray(features) ? features : [features], }), }); } catch (error) { diff --git a/x-pack/plugins/cases/server/routes/api/metrics/get_cases_metrics.ts b/x-pack/plugins/cases/server/routes/api/metrics/get_cases_metrics.ts index 3eb9ec26a9297..44d351571edbb 100644 --- a/x-pack/plugins/cases/server/routes/api/metrics/get_cases_metrics.ts +++ b/x-pack/plugins/cases/server/routes/api/metrics/get_cases_metrics.ts @@ -16,7 +16,10 @@ export const getCasesMetricRoute = createCasesRoute({ path: CASE_METRICS_URL, params: { query: schema.object({ - features: schema.arrayOf(schema.string({ minLength: 1 })), + features: schema.oneOf([ + schema.arrayOf(schema.string({ minLength: 1 })), + schema.string({ minLength: 1 }), + ]), owner: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])), from: schema.maybe(schema.string()), to: schema.maybe(schema.string()), @@ -26,9 +29,12 @@ export const getCasesMetricRoute = createCasesRoute({ try { const caseContext = await context.cases; const client = await caseContext.getCasesClient(); + const { features } = request.query; + return response.ok({ body: await client.metrics.getCasesMetrics({ ...request.query, + features: Array.isArray(features) ? features : [features], }), }); } catch (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 ea68fc24f60ca..9b2ea975c4dcd 100644 --- a/x-pack/plugins/cases/server/saved_object_types/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/cases.ts @@ -152,6 +152,9 @@ export const createCaseSavedObjectType = ( }, }, }, + severity: { + type: 'keyword', + }, }, }, migrations: caseMigrations, 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 70e0e91caa57f..b4d3421643a41 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 @@ -9,13 +9,14 @@ import { SavedObjectSanitizedDoc } from '@kbn/core/server'; import { CaseAttributes, CaseFullExternalService, + CaseSeverity, ConnectorTypes, NONE_CONNECTOR_ID, } from '../../../common/api'; import { CASE_SAVED_OBJECT } from '../../../common/constants'; import { getNoneCaseConnector } from '../../common/utils'; import { createExternalService, ESCaseConnectorWithId } from '../../services/test_utils'; -import { addDuration, caseConnectorIdMigration, removeCaseType } from './cases'; +import { addDuration, addSeverity, caseConnectorIdMigration, removeCaseType } from './cases'; // eslint-disable-next-line @typescript-eslint/naming-convention const create_7_14_0_case = ({ @@ -496,4 +497,45 @@ describe('case migrations', () => { }); }); }); + + describe('add severity', () => { + it('adds the severity correctly when none is present', () => { + const doc = { + id: '123', + attributes: { + created_at: '2021-11-23T19:00:00Z', + closed_at: '2021-11-23T19:02:00Z', + }, + type: 'abc', + references: [], + } as unknown as SavedObjectSanitizedDoc; + expect(addSeverity(doc)).toEqual({ + ...doc, + attributes: { + ...doc.attributes, + severity: CaseSeverity.LOW, + }, + }); + }); + + it('keeps the existing value if the field already exists', () => { + const doc = { + id: '123', + attributes: { + severity: CaseSeverity.CRITICAL, + created_at: '2021-11-23T19:00:00Z', + closed_at: '2021-11-23T19:02:00Z', + }, + type: 'abc', + references: [], + } as unknown as SavedObjectSanitizedDoc; + expect(addSeverity(doc)).toEqual({ + ...doc, + attributes: { + ...doc.attributes, + severity: CaseSeverity.CRITICAL, + }, + }); + }); + }); }); 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 91a462c5c8053..c4961f742abc7 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 @@ -11,7 +11,7 @@ import { cloneDeep, unset } from 'lodash'; import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc } from '@kbn/core/server'; import { addOwnerToSO, SanitizedCaseOwner } from '.'; import { ESConnectorFields } from '../../services'; -import { CaseAttributes, ConnectorTypes } from '../../../common/api'; +import { CaseAttributes, CaseSeverity, ConnectorTypes } from '../../../common/api'; import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME, @@ -21,6 +21,7 @@ import { transformPushConnectorIdToReference, } from './user_actions/connector_id'; import { CASE_TYPE_INDIVIDUAL } from './constants'; +import { pipeMigrations } from './utils'; interface UnsanitizedCaseConnector { connector_id: string; @@ -114,6 +115,13 @@ export const addDuration = ( return { ...doc, attributes: { ...doc.attributes, duration }, references: doc.references ?? [] }; }; +export const addSeverity = ( + doc: SavedObjectUnsanitizedDoc +): SavedObjectSanitizedDoc => { + const severity = doc.attributes.severity ?? CaseSeverity.LOW; + return { ...doc, attributes: { ...doc.attributes, severity }, references: doc.references ?? [] }; +}; + export const caseMigrations = { '7.10.0': ( doc: SavedObjectUnsanitizedDoc @@ -175,5 +183,5 @@ export const caseMigrations = { }, '7.15.0': caseConnectorIdMigration, '8.1.0': removeCaseType, - '8.3.0': addDuration, + '8.3.0': pipeMigrations(addDuration, addSeverity), }; 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 index 65c1d42271845..8996f89155949 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LogMeta, SavedObjectMigrationContext } from '@kbn/core/server'; +import { LogMeta, SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from '@kbn/core/server'; interface MigrationLogMeta extends LogMeta { migrations: { @@ -39,3 +39,10 @@ export function logError({ } ); } + +type CaseMigration = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc; + +export function pipeMigrations(...migrations: Array>): CaseMigration { + return (doc: SavedObjectUnsanitizedDoc) => + migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); +} 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 84c580c8800e3..826a8d06e97f2 100644 --- a/x-pack/plugins/cases/server/services/cases/index.test.ts +++ b/x-pack/plugins/cases/server/services/cases/index.test.ts @@ -166,6 +166,7 @@ describe('CasesService', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "defacement", @@ -519,6 +520,7 @@ describe('CasesService', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "defacement", diff --git a/x-pack/plugins/cases/server/services/test_utils.ts b/x-pack/plugins/cases/server/services/test_utils.ts index 617dedd368ab3..ff86783ae8e9c 100644 --- a/x-pack/plugins/cases/server/services/test_utils.ts +++ b/x-pack/plugins/cases/server/services/test_utils.ts @@ -10,15 +10,18 @@ import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server'; import { ESConnectorFields } from '.'; import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../common/constants'; import { + CaseAttributes, CaseConnector, CaseExternalServiceBasic, CaseFullExternalService, + CaseSeverity, CaseStatuses, ConnectorTypes, NONE_CONNECTOR_ID, } from '../../common/api'; import { CASE_SAVED_OBJECT, SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './cases/types'; +import { getNoneCaseConnector } from '../common/utils'; /** * This is only a utility interface to help with constructing test cases. After the migration, the ES format will no longer @@ -96,7 +99,7 @@ export const createExternalService = ( ...overrides, }); -export const basicCaseFields = { +export const basicCaseFields: CaseAttributes = { closed_at: null, closed_by: null, created_at: '2019-11-25T21:54:48.952Z', @@ -105,6 +108,7 @@ export const basicCaseFields = { email: 'testemail@elastic.co', username: 'elastic', }, + severity: CaseSeverity.LOW, duration: null, description: 'This is a brand new case of a bad meanie defacing data', title: 'Super Bad Security Issue', @@ -116,6 +120,8 @@ export const basicCaseFields = { email: 'testemail@elastic.co', username: 'elastic', }, + connector: getNoneCaseConnector(), + external_service: null, settings: { syncAlerts: true, }, diff --git a/x-pack/plugins/cases/server/services/user_actions/builder_factory.test.ts b/x-pack/plugins/cases/server/services/user_actions/builder_factory.test.ts index 2e2a9e905bb7e..ab349d690edef 100644 --- a/x-pack/plugins/cases/server/services/user_actions/builder_factory.test.ts +++ b/x-pack/plugins/cases/server/services/user_actions/builder_factory.test.ts @@ -9,6 +9,7 @@ import { SECURITY_SOLUTION_OWNER } from '../../../common'; import { Actions, ActionTypes, + CaseSeverity, CaseStatuses, CommentType, ConnectorTypes, @@ -340,6 +341,40 @@ describe('UserActionBuilder', () => { `); }); + it('builds a severity user action correctly', () => { + const builder = builderFactory.getBuilder(ActionTypes.severity)!; + const userAction = builder.build({ + payload: { severity: CaseSeverity.LOW }, + ...commonArgs, + }); + + expect(userAction).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "action": "update", + "created_at": "2022-01-09T22:00:00.000Z", + "created_by": Object { + "email": "elastic@elastic.co", + "full_name": "Elastic User", + "username": "elastic", + }, + "owner": "securitySolution", + "payload": Object { + "severity": "low", + }, + "type": "severity", + }, + "references": Array [ + Object { + "id": "123", + "name": "associated-cases", + "type": "cases", + }, + ], + } + `); + }); + it('builds a settings user action correctly', () => { const builder = builderFactory.getBuilder(ActionTypes.settings)!; const userAction = builder.build({ @@ -413,6 +448,7 @@ describe('UserActionBuilder', () => { "settings": Object { "syncAlerts": true, }, + "severity": "low", "status": "open", "tags": Array [ "sir", diff --git a/x-pack/plugins/cases/server/services/user_actions/builder_factory.ts b/x-pack/plugins/cases/server/services/user_actions/builder_factory.ts index 5d5f33c2ae4f5..510b6d12b1fa1 100644 --- a/x-pack/plugins/cases/server/services/user_actions/builder_factory.ts +++ b/x-pack/plugins/cases/server/services/user_actions/builder_factory.ts @@ -17,6 +17,7 @@ import { TagsUserActionBuilder } from './builders/tags'; import { SettingsUserActionBuilder } from './builders/settings'; import { DeleteCaseUserActionBuilder } from './builders/delete_case'; import { UserActionBuilder } from './abstract_builder'; +import { SeverityUserActionBuilder } from './builders/severity'; const builderMap = { title: TitleUserActionBuilder, @@ -27,6 +28,7 @@ const builderMap = { pushed: PushedUserActionBuilder, tags: TagsUserActionBuilder, status: StatusUserActionBuilder, + severity: SeverityUserActionBuilder, settings: SettingsUserActionBuilder, delete_case: DeleteCaseUserActionBuilder, }; diff --git a/x-pack/plugins/cases/server/services/user_actions/builders/severity.ts b/x-pack/plugins/cases/server/services/user_actions/builders/severity.ts new file mode 100644 index 0000000000000..4abd5856972b4 --- /dev/null +++ b/x-pack/plugins/cases/server/services/user_actions/builders/severity.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 { Actions, ActionTypes } from '../../../../common/api'; +import { UserActionBuilder } from '../abstract_builder'; +import { UserActionParameters, BuilderReturnValue } from '../types'; + +export class SeverityUserActionBuilder extends UserActionBuilder { + build(args: UserActionParameters<'severity'>): BuilderReturnValue { + return this.buildCommonUserAction({ + ...args, + action: Actions.update, + valueKey: 'severity', + value: args.payload.severity, + type: ActionTypes.severity, + }); + } +} 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 index eb1b57622d24d..44e91bcae09d3 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.test.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.test.ts @@ -13,6 +13,7 @@ import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server'; import { Actions, ActionTypes, + CaseSeverity, CaseStatuses, CaseUserActionAttributes, ConnectorUserAction, @@ -107,6 +108,7 @@ const createCaseUserAction = (): SavedObject => { description: 'a desc', settings: { syncAlerts: false }, status: CaseStatuses.open, + severity: CaseSeverity.LOW, tags: [], owner: SECURITY_SOLUTION_OWNER, }, @@ -447,6 +449,7 @@ describe('CaseUserActionService', () => { payload: casePayload, type: ActionTypes.create_case, }); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( 'cases-user-actions', { @@ -477,6 +480,7 @@ describe('CaseUserActionService', () => { owner: 'securitySolution', settings: { syncAlerts: true }, status: 'open', + severity: 'low', tags: ['sir'], title: 'Case SIR', }, @@ -517,6 +521,33 @@ describe('CaseUserActionService', () => { }); }); + describe('severity', () => { + it('creates an update severity user action', async () => { + await service.createUserAction({ + ...commonArgs, + payload: { severity: CaseSeverity.MEDIUM }, + type: ActionTypes.severity, + }); + + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( + 'cases-user-actions', + { + action: Actions.update, + created_at: '2022-01-09T22:00:00.000Z', + created_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic User', + username: 'elastic', + }, + type: 'severity', + owner: 'securitySolution', + payload: { severity: 'medium' }, + }, + { references: [{ id: '123', name: 'associated-cases', type: 'cases' }] } + ); + }); + }); + describe('push', () => { it('creates a push user action', async () => { await service.createUserAction({ @@ -801,6 +832,30 @@ describe('CaseUserActionService', () => { references: [{ id: '2', name: 'associated-cases', type: 'cases' }], type: 'cases-user-actions', }, + { + attributes: { + action: 'update', + created_at: '2022-01-09T22:00:00.000Z', + created_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic User', + username: 'elastic', + }, + owner: 'securitySolution', + payload: { + severity: 'critical', + }, + type: 'severity', + }, + references: [ + { + id: '2', + name: 'associated-cases', + type: 'cases', + }, + ], + type: 'cases-user-actions', + }, ]); }); }); diff --git a/x-pack/plugins/cases/server/services/user_actions/mocks.ts b/x-pack/plugins/cases/server/services/user_actions/mocks.ts index c745c040ac2ce..bc35f98bf926e 100644 --- a/x-pack/plugins/cases/server/services/user_actions/mocks.ts +++ b/x-pack/plugins/cases/server/services/user_actions/mocks.ts @@ -7,7 +7,7 @@ import { CASE_SAVED_OBJECT } from '../../../common/constants'; import { SECURITY_SOLUTION_OWNER } from '../../../common'; -import { CaseStatuses, CommentType, ConnectorTypes } from '../../../common/api'; +import { CaseSeverity, CaseStatuses, CommentType, ConnectorTypes } from '../../../common/api'; import { createCaseSavedObjectResponse } from '../test_utils'; import { transformSavedObjectToExternalModel } from '../cases/transform'; @@ -30,6 +30,7 @@ export const casePayload = { }, }, settings: { syncAlerts: true }, + severity: CaseSeverity.LOW, owner: SECURITY_SOLUTION_OWNER, }; @@ -69,6 +70,7 @@ export const updatedCases = [ description: 'updated desc', tags: ['one', 'two'], settings: { syncAlerts: false }, + severity: CaseSeverity.CRITICAL, }, 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 index f681a9186181c..a60dee552a6be 100644 --- a/x-pack/plugins/cases/server/services/user_actions/types.ts +++ b/x-pack/plugins/cases/server/services/user_actions/types.ts @@ -9,6 +9,7 @@ import { SavedObjectReference } from '@kbn/core/server'; import { CasePostRequest, CaseSettings, + CaseSeverity, CaseStatuses, CommentUserAction, ConnectorUserAction, @@ -28,6 +29,9 @@ export interface BuilderParameters { status: { parameters: { payload: { status: CaseStatuses } }; }; + severity: { + parameters: { payload: { severity: CaseSeverity } }; + }; tags: { parameters: { payload: { tags: string[] } }; }; diff --git a/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.test.ts b/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.test.ts new file mode 100644 index 0000000000000..a6dc1f59b00e3 --- /dev/null +++ b/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.test.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 { firstValueFrom } from 'rxjs'; +import { registerCloudDeploymentIdAnalyticsContext } from './register_cloud_deployment_id_analytics_context'; + +describe('registerCloudDeploymentIdAnalyticsContext', () => { + let analytics: { registerContextProvider: jest.Mock }; + beforeEach(() => { + analytics = { + registerContextProvider: jest.fn(), + }; + }); + + test('it does not register the context provider if cloudId not provided', () => { + registerCloudDeploymentIdAnalyticsContext(analytics); + expect(analytics.registerContextProvider).not.toHaveBeenCalled(); + }); + + test('it registers the context provider and emits the cloudId', async () => { + registerCloudDeploymentIdAnalyticsContext(analytics, 'cloud_id'); + expect(analytics.registerContextProvider).toHaveBeenCalledTimes(1); + const [{ context$ }] = analytics.registerContextProvider.mock.calls[0]; + await expect(firstValueFrom(context$)).resolves.toEqual({ cloudId: 'cloud_id' }); + }); +}); diff --git a/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts b/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts new file mode 100644 index 0000000000000..e8bdc6b37b50c --- /dev/null +++ b/x-pack/plugins/cloud/common/register_cloud_deployment_id_analytics_context.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AnalyticsClient } from '@kbn/analytics-client'; +import { of } from 'rxjs'; + +export function registerCloudDeploymentIdAnalyticsContext( + analytics: Pick, + cloudId?: string +) { + if (!cloudId) { + return; + } + analytics.registerContextProvider({ + name: 'Cloud Deployment ID', + context$: of({ cloudId }), + schema: { + cloudId: { + type: 'keyword', + _meta: { description: 'The Cloud Deployment ID' }, + }, + }, + }); +} diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts index 5e0294178a5da..36be9e590f216 100644 --- a/x-pack/plugins/cloud/public/plugin.test.ts +++ b/x-pack/plugins/cloud/public/plugin.test.ts @@ -9,9 +9,9 @@ import { nextTick } from '@kbn/test-jest-helpers'; import { coreMock } from '@kbn/core/public/mocks'; import { homePluginMock } from '@kbn/home-plugin/public/mocks'; import { securityMock } from '@kbn/security-plugin/public/mocks'; -import { CloudPlugin, CloudConfigType, loadUserId } from './plugin'; -import { firstValueFrom, Observable, Subject } from 'rxjs'; -import { KibanaExecutionContext } from '@kbn/core/public'; +import { CloudPlugin, CloudConfigType } from './plugin'; +import { firstValueFrom } from 'rxjs'; +import { Sha256 } from '@kbn/core/public/utils'; describe('Cloud Plugin', () => { describe('#setup', () => { @@ -20,17 +20,7 @@ describe('Cloud Plugin', () => { jest.clearAllMocks(); }); - const setupPlugin = async ({ - config = {}, - securityEnabled = true, - currentUserProps = {}, - currentContext$ = undefined, - }: { - config?: Partial; - securityEnabled?: boolean; - currentUserProps?: Record; - currentContext$?: Observable; - }) => { + const setupPlugin = async ({ config = {} }: { config?: Partial }) => { const initContext = coreMock.createPluginInitializerContext({ id: 'cloudId', base_url: 'https://cloud.elastic.co', @@ -49,21 +39,9 @@ describe('Cloud Plugin', () => { const plugin = new CloudPlugin(initContext); const coreSetup = coreMock.createSetup(); - const coreStart = coreMock.createStart(); - if (currentContext$) { - coreStart.executionContext.context$ = currentContext$; - } - - coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]); - - const securitySetup = securityMock.createSetup(); - - securitySetup.authc.getCurrentUser.mockResolvedValue( - securityMock.createMockAuthenticatedUser(currentUserProps) - ); + const setup = plugin.setup(coreSetup, {}); - const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {}); // Wait for FullStory dynamic import to resolve await new Promise((r) => setImmediate(r)); @@ -73,9 +51,6 @@ describe('Cloud Plugin', () => { test('register the shipper FullStory with correct args when enabled and org_id are set', async () => { const { coreSetup } = await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' } }, - currentUserProps: { - username: '1234', - }, }); expect(coreSetup.analytics.registerShipper).toHaveBeenCalled(); @@ -86,12 +61,71 @@ describe('Cloud Plugin', () => { }); }); - test('register the context provider for the cloud user with hashed user ID when security is available', async () => { + it('does not call initializeFullStory when enabled=false', async () => { const { coreSetup } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' } }, - currentUserProps: { - username: '1234', + config: { full_story: { enabled: false, org_id: 'foo' } }, + }); + expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled(); + }); + + it('does not call initializeFullStory when org_id is undefined', async () => { + const { coreSetup } = await setupPlugin({ config: { full_story: { enabled: true } } }); + expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled(); + }); + }); + + describe('setupTelemetryContext', () => { + const username = '1234'; + const expectedHashedPlainUsername = new Sha256().update(username, 'utf8').digest('hex'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const setupPlugin = async ({ + config = {}, + securityEnabled = true, + currentUserProps = {}, + }: { + config?: Partial; + securityEnabled?: boolean; + currentUserProps?: Record | Error; + }) => { + const initContext = coreMock.createPluginInitializerContext({ + base_url: 'https://cloud.elastic.co', + deployment_url: '/abc123', + profile_url: '/profile/alice', + organization_url: '/org/myOrg', + full_story: { + enabled: false, }, + chat: { + enabled: false, + }, + ...config, + }); + + const plugin = new CloudPlugin(initContext); + + const coreSetup = coreMock.createSetup(); + const securitySetup = securityMock.createSetup(); + if (currentUserProps instanceof Error) { + securitySetup.authc.getCurrentUser.mockRejectedValue(currentUserProps); + } else { + securitySetup.authc.getCurrentUser.mockResolvedValue( + securityMock.createMockAuthenticatedUser(currentUserProps) + ); + } + + const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {}); + + return { initContext, plugin, setup, coreSetup }; + }; + + test('register the context provider for the cloud user with hashed user ID when security is available', async () => { + const { coreSetup } = await setupPlugin({ + config: { id: 'cloudId' }, + currentUserProps: { username }, }); expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled(); @@ -105,12 +139,10 @@ describe('Cloud Plugin', () => { }); }); - it('user hash includes org id', async () => { + it('user hash includes cloud id', async () => { const { coreSetup: coreSetup1 } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg1' }, - currentUserProps: { - username: '1234', - }, + config: { id: 'esOrg1' }, + currentUserProps: { username }, }); const [{ context$: context1$ }] = @@ -119,12 +151,11 @@ describe('Cloud Plugin', () => { )!; const hashId1 = await firstValueFrom(context1$); + expect(hashId1).not.toEqual(expectedHashedPlainUsername); const { coreSetup: coreSetup2 } = await setupPlugin({ config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg2' }, - currentUserProps: { - username: '1234', - }, + currentUserProps: { username }, }); const [{ context$: context2$ }] = @@ -133,150 +164,60 @@ describe('Cloud Plugin', () => { )!; const hashId2 = await firstValueFrom(context2$); + expect(hashId2).not.toEqual(expectedHashedPlainUsername); expect(hashId1).not.toEqual(hashId2); }); - it('emits the execution context provider everytime an app changes', async () => { - const currentContext$ = new Subject(); + test('user hash does not include cloudId when authenticated via Cloud SAML', async () => { const { coreSetup } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' } }, + config: { id: 'cloudDeploymentId' }, currentUserProps: { - username: '1234', + username, + authentication_realm: { type: 'saml', name: 'cloud-saml-kibana' }, }, - currentContext$, }); + expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled(); + const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find( - ([{ name }]) => name === 'execution_context' + ([{ name }]) => name === 'cloud_user_id' )!; - let latestContext; - context$.subscribe((context) => { - latestContext = context; - }); - - // takes the app name - expect(latestContext).toBeUndefined(); - currentContext$.next({ - name: 'App1', - description: '123', - }); - - await new Promise((r) => setImmediate(r)); - - expect(latestContext).toEqual({ - pageName: 'App1', - applicationId: 'App1', - }); - - // context clear - currentContext$.next({}); - expect(latestContext).toEqual({ - pageName: '', - applicationId: 'unknown', - }); - - // different app - currentContext$.next({ - name: 'App2', - page: 'page2', - id: '123', - }); - expect(latestContext).toEqual({ - pageName: 'App2:page2', - applicationId: 'App2', - page: 'page2', - entityId: '123', - }); - - // Back to first app - currentContext$.next({ - name: 'App1', - page: 'page3', - id: '123', - }); - - expect(latestContext).toEqual({ - pageName: 'App1:page3', - applicationId: 'App1', - page: 'page3', - entityId: '123', + await expect(firstValueFrom(context$)).resolves.toEqual({ + userId: expectedHashedPlainUsername, }); }); - it('does not register the cloud user id context provider when security is not available', async () => { + test('user hash does not include cloudId when not provided', async () => { const { coreSetup } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' } }, - securityEnabled: false, + config: {}, + currentUserProps: { username }, }); - expect( - coreSetup.analytics.registerContextProvider.mock.calls.find( - ([{ name }]) => name === 'cloud_user_id' - ) - ).toBeUndefined(); - }); - - describe('with memory', () => { - beforeAll(() => { - // @ts-expect-error 2339 - window.performance.memory = { - get jsHeapSizeLimit() { - return 3; - }, - get totalJSHeapSize() { - return 2; - }, - get usedJSHeapSize() { - return 1; - }, - }; - }); - - afterAll(() => { - // @ts-expect-error 2339 - delete window.performance.memory; - }); + expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled(); - it('reports an event when security is available', async () => { - const { initContext, coreSetup } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' } }, - currentUserProps: { - username: '1234', - }, - }); + const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find( + ([{ name }]) => name === 'cloud_user_id' + )!; - expect(coreSetup.analytics.reportEvent).toHaveBeenCalledWith('Loaded Kibana', { - kibana_version: initContext.env.packageInfo.version, - memory_js_heap_size_limit: 3, - memory_js_heap_size_total: 2, - memory_js_heap_size_used: 1, - }); + await expect(firstValueFrom(context$)).resolves.toEqual({ + userId: expectedHashedPlainUsername, }); }); - it('reports an event when security is not available', async () => { - const { initContext, coreSetup } = await setupPlugin({ - config: { full_story: { enabled: true, org_id: 'foo' } }, - securityEnabled: false, + test('user hash is undefined when failed to fetch a user', async () => { + const { coreSetup } = await setupPlugin({ + currentUserProps: new Error('failed to fetch a user'), }); - expect(coreSetup.analytics.reportEvent).toHaveBeenCalledWith('Loaded Kibana', { - kibana_version: initContext.env.packageInfo.version, - }); - }); + expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled(); - it('does not call initializeFullStory when enabled=false', async () => { - const { coreSetup } = await setupPlugin({ - config: { full_story: { enabled: false, org_id: 'foo' } }, - }); - expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled(); - }); + const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find( + ([{ name }]) => name === 'cloud_user_id' + )!; - it('does not call initializeFullStory when org_id is undefined', async () => { - const { coreSetup } = await setupPlugin({ config: { full_story: { enabled: true } } }); - expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled(); + await expect(firstValueFrom(context$)).resolves.toEqual({ userId: undefined }); }); }); @@ -652,56 +593,4 @@ describe('Cloud Plugin', () => { expect(securityStart.navControlService.addUserMenuLinks).not.toHaveBeenCalled(); }); }); - - describe('loadFullStoryUserId', () => { - let consoleMock: jest.SpyInstance; - - beforeEach(() => { - consoleMock = jest.spyOn(console, 'debug').mockImplementation(() => {}); - }); - afterEach(() => { - consoleMock.mockRestore(); - }); - - it('returns principal ID when username specified', async () => { - expect( - await loadUserId({ - getCurrentUser: jest.fn().mockResolvedValue({ - username: '1234', - }), - }) - ).toEqual('1234'); - expect(consoleMock).not.toHaveBeenCalled(); - }); - - it('returns undefined if getCurrentUser throws', async () => { - expect( - await loadUserId({ - getCurrentUser: jest.fn().mockRejectedValue(new Error(`Oh no!`)), - }) - ).toBeUndefined(); - }); - - it('returns undefined if getCurrentUser returns undefined', async () => { - expect( - await loadUserId({ - getCurrentUser: jest.fn().mockResolvedValue(undefined), - }) - ).toBeUndefined(); - }); - - it('returns undefined and logs if username undefined', async () => { - expect( - await loadUserId({ - getCurrentUser: jest.fn().mockResolvedValue({ - username: undefined, - metadata: { foo: 'bar' }, - }), - }) - ).toBeUndefined(); - expect(consoleMock).toHaveBeenLastCalledWith( - `[cloud.analytics] username not specified. User metadata: {"foo":"bar"}` - ); - }); - }); }); diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx index bffc1d9496422..1bccf219225dc 100644 --- a/x-pack/plugins/cloud/public/plugin.tsx +++ b/x-pack/plugins/cloud/public/plugin.tsx @@ -13,21 +13,16 @@ import type { PluginInitializerContext, HttpStart, IBasePath, - ExecutionContextStart, AnalyticsServiceSetup, } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import useObservable from 'react-use/lib/useObservable'; -import { BehaviorSubject, from, of, Subscription } from 'rxjs'; -import { exhaustMap, filter, map } from 'rxjs/operators'; -import { compact } from 'lodash'; +import { BehaviorSubject, catchError, from, map, of } from 'rxjs'; -import type { - AuthenticatedUser, - SecurityPluginSetup, - SecurityPluginStart, -} from '@kbn/security-plugin/public'; +import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; +import { Sha256 } from '@kbn/core/public/utils'; +import { registerCloudDeploymentIdAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { ELASTIC_SUPPORT_LINK, @@ -49,6 +44,7 @@ export interface CloudConfigType { full_story: { enabled: boolean; org_id?: string; + eventTypesAllowlist?: string[]; }; /** Configuration to enable live chat in Cloud-enabled instances of Kibana. */ chat: { @@ -90,11 +86,6 @@ interface SetupFullStoryDeps { analytics: AnalyticsServiceSetup; basePath: IBasePath; } -interface SetupTelemetryContextDeps extends CloudSetupDependencies { - analytics: AnalyticsServiceSetup; - executionContextPromise: Promise; - cloudId?: string; -} interface SetupChatDeps extends Pick { http: CoreSetup['http']; @@ -103,7 +94,6 @@ interface SetupChatDeps extends Pick { export class CloudPlugin implements Plugin { private readonly config: CloudConfigType; private isCloudEnabled: boolean; - private appSubscription?: Subscription; private chatConfig$ = new BehaviorSubject({ enabled: false }); constructor(private readonly initializerContext: PluginInitializerContext) { @@ -112,19 +102,7 @@ export class CloudPlugin implements Plugin { } public setup(core: CoreSetup, { home, security }: CloudSetupDependencies) { - const executionContextPromise = core.getStartServices().then(([coreStart]) => { - return coreStart.executionContext; - }); - - this.setupTelemetryContext({ - analytics: core.analytics, - security, - executionContextPromise, - cloudId: this.config.id, - }).catch((e) => { - // eslint-disable-next-line no-console - console.debug(`Error setting up TelemetryContext: ${e.toString()}`); - }); + this.setupTelemetryContext(core.analytics, security, this.config.id); this.setupFullStory({ analytics: core.analytics, basePath: core.http.basePath }).catch((e) => // eslint-disable-next-line no-console @@ -212,9 +190,7 @@ export class CloudPlugin implements Plugin { }; } - public stop() { - this.appSubscription?.unsubscribe(); - } + public stop() {} /** * Determines if the current user should see links back to Cloud. @@ -249,7 +225,7 @@ export class CloudPlugin implements Plugin { * @private */ private async setupFullStory({ analytics, basePath }: SetupFullStoryDeps) { - const { enabled, org_id: fullStoryOrgId } = this.config.full_story; + const { enabled, org_id: fullStoryOrgId, eventTypesAllowlist } = this.config.full_story; if (!enabled || !fullStoryOrgId) { return; // do not load any FullStory code in the browser if not enabled } @@ -257,6 +233,7 @@ export class CloudPlugin implements Plugin { // Keep this import async so that we do not load any FullStory code into the browser when it is disabled. const { FullStoryShipper } = await import('@kbn/analytics-shippers-fullstory'); analytics.registerShipper(FullStoryShipper, { + eventTypesAllowlist, fullStoryOrgId, // Load an Elastic-internally audited script. Ideally, it should be hosted on a CDN. scriptUrl: basePath.prepend( @@ -270,48 +247,36 @@ export class CloudPlugin implements Plugin { * Set up the Analytics context providers. * @param analytics Core's Analytics service. The Setup contract. * @param security The security plugin. - * @param executionContextPromise Core's executionContext's start contract. - * @param esOrgId The Cloud Org ID. + * @param cloudId The Cloud Org ID. * @private */ - private async setupTelemetryContext({ - analytics, - security, - executionContextPromise, - cloudId, - }: SetupTelemetryContextDeps) { - // Some context providers can be moved to other places for better domain isolation. - // Let's use https://github.com/elastic/kibana/issues/125690 for that purpose. - analytics.registerContextProvider({ - name: 'kibana_version', - context$: of({ version: this.initializerContext.env.packageInfo.version }), - schema: { version: { type: 'keyword', _meta: { description: 'The version of Kibana' } } }, - }); + private setupTelemetryContext( + analytics: AnalyticsServiceSetup, + security?: Pick, + cloudId?: string + ) { + registerCloudDeploymentIdAnalyticsContext(analytics, cloudId); - analytics.registerContextProvider({ - name: 'cloud_org_id', - context$: of({ cloudId }), - schema: { - cloudId: { - type: 'keyword', - _meta: { description: 'The Cloud ID', optional: true }, - }, - }, - }); - - // This needs to be called synchronously to be sure that we populate the user ID soon enough to make sessions merging - // across domains work if (security) { analytics.registerContextProvider({ name: 'cloud_user_id', - context$: from(loadUserId({ getCurrentUser: security.authc.getCurrentUser })).pipe( - filter((userId): userId is string => Boolean(userId)), - exhaustMap(async (userId) => { - const { sha256 } = await import('js-sha256'); - // Join the cloud org id and the user to create a truly unique user id. - // The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs - return { userId: sha256(cloudId ? `${cloudId}:${userId}` : `${userId}`) }; - }) + context$: from(security.authc.getCurrentUser()).pipe( + map((user) => { + if ( + getIsCloudEnabled(cloudId) && + user.authentication_realm?.type === 'saml' && + user.authentication_realm?.name === 'cloud-saml-kibana' + ) { + // If authenticated via Cloud SAML, use the SAML username as the user ID + return user.username; + } + + return cloudId ? `${cloudId}:${user.username}` : user.username; + }), + // Join the cloud org id and the user to create a truly unique user id. + // The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs + map((userId) => ({ userId: sha256(userId) })), + catchError(() => of({ userId: undefined })) ), schema: { userId: { @@ -321,81 +286,6 @@ export class CloudPlugin implements Plugin { }, }); } - - const executionContext = await executionContextPromise; - analytics.registerContextProvider({ - name: 'execution_context', - context$: executionContext.context$.pipe( - // Update the current context every time it changes - map(({ name, page, id }) => ({ - pageName: `${compact([name, page]).join(':')}`, - applicationId: name ?? 'unknown', - page, - entityId: id, - })) - ), - schema: { - pageName: { - type: 'keyword', - _meta: { description: 'The name of the current page' }, - }, - page: { - type: 'keyword', - _meta: { description: 'The current page', optional: true }, - }, - applicationId: { - type: 'keyword', - _meta: { description: 'The id of the current application' }, - }, - entityId: { - type: 'keyword', - _meta: { - description: - 'The id of the current entity (dashboard, visualization, canvas, lens, etc)', - optional: true, - }, - }, - }, - }); - - analytics.registerEventType({ - eventType: 'Loaded Kibana', - schema: { - kibana_version: { - type: 'keyword', - _meta: { description: 'The version of Kibana', optional: true }, - }, - memory_js_heap_size_limit: { - type: 'long', - _meta: { description: 'The maximum size of the heap', optional: true }, - }, - memory_js_heap_size_total: { - type: 'long', - _meta: { description: 'The total size of the heap', optional: true }, - }, - memory_js_heap_size_used: { - type: 'long', - _meta: { description: 'The used size of the heap', optional: true }, - }, - }, - }); - - // Get performance information from the browser (non standard property - // @ts-expect-error 2339 - const memory = window.performance.memory; - let memoryInfo = {}; - if (memory) { - memoryInfo = { - memory_js_heap_size_limit: memory.jsHeapSizeLimit, - memory_js_heap_size_total: memory.totalJSHeapSize, - memory_js_heap_size_used: memory.usedJSHeapSize, - }; - } - - analytics.reportEvent('Loaded Kibana', { - kibana_version: this.initializerContext.env.packageInfo.version, - ...memoryInfo, - }); } private async setupChat({ http, security }: SetupChatDeps) { @@ -436,32 +326,6 @@ export class CloudPlugin implements Plugin { } } -/** @internal exported for testing */ -export const loadUserId = async ({ - getCurrentUser, -}: { - getCurrentUser: () => Promise; -}) => { - try { - const currentUser = await getCurrentUser().catch(() => undefined); - if (!currentUser) { - return undefined; - } - - // Log very defensively here so we can debug this easily if it breaks - if (!currentUser.username) { - // eslint-disable-next-line no-console - console.debug( - `[cloud.analytics] username not specified. User metadata: ${JSON.stringify( - currentUser.metadata - )}` - ); - } - - return currentUser.username; - } catch (e) { - // eslint-disable-next-line no-console - console.error(`[cloud.analytics] Error loading the current user: ${e.toString()}`, e); - return undefined; - } -}; +function sha256(str: string) { + return new Sha256().update(str, 'utf8').digest('hex'); +} diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index bee83e23475b5..aebbc65e50f18 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -26,6 +26,9 @@ const fullStoryConfigSchema = schema.object({ schema.string({ minLength: 1 }), schema.maybe(schema.string()) ), + eventTypesAllowlist: schema.arrayOf(schema.string(), { + defaultValue: ['Loaded Kibana'], + }), }); const chatConfigSchema = schema.object({ diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index 284d37804be21..2cbb41531ecf5 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -8,6 +8,7 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server'; import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; +import { registerCloudDeploymentIdAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context'; import { CloudConfigType } from './config'; import { registerCloudUsageCollector } from './collectors'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; @@ -35,7 +36,7 @@ export interface CloudSetup { export class CloudPlugin implements Plugin { private readonly logger: Logger; private readonly config: CloudConfigType; - private isDev: boolean; + private readonly isDev: boolean; constructor(private readonly context: PluginInitializerContext) { this.logger = this.context.logger.get(); @@ -46,6 +47,7 @@ export class CloudPlugin implements Plugin { public setup(core: CoreSetup, { usageCollection, security }: PluginsSetup): CloudSetup { this.logger.debug('Setting up Cloud plugin'); const isCloudEnabled = getIsCloudEnabled(this.config.id); + registerCloudDeploymentIdAnalyticsContext(core.analytics, this.config.id); registerCloudUsageCollector(usageCollection, { isCloudEnabled }); if (this.config.full_story.enabled) { diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 1d35d6439bead..30e9651b6e739 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -31,5 +31,7 @@ export const INTERNAL_FEATURE_FLAGS = { showBenchmarks: true, showManageRulesMock: false, showRisksMock: false, - showFindingsGroupBy: false, + showFindingsGroupBy: true, } as const; + +export const cspRuleAssetSavedObjectType = 'csp_rule'; diff --git a/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule.ts b/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule.ts index a6aaa26e7a1a0..cdefc461cd952 100644 --- a/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule.ts +++ b/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule.ts @@ -6,8 +6,6 @@ */ import { schema as rt, TypeOf } from '@kbn/config-schema'; -export const cspRuleAssetSavedObjectType = 'csp_rule'; - // TODO: needs to be shared with cloudbeat export const cspRuleSchema = rt.object({ id: rt.string(), diff --git a/x-pack/plugins/cloud_security_posture/common/types.ts b/x-pack/plugins/cloud_security_posture/common/types.ts index 4bc388d39ebe4..110e8c071deff 100644 --- a/x-pack/plugins/cloud_security_posture/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/common/types.ts @@ -47,6 +47,12 @@ export interface ComplianceDashboardData { trend: PostureTrend[]; } +export interface CspRulesStatus { + all: number; + enabled: number; + disabled: number; +} + export interface Benchmark { package_policy: Pick< PackagePolicy, @@ -61,4 +67,5 @@ export interface Benchmark { | 'created_by' >; agent_policy: Pick; + rules: CspRulesStatus; } diff --git a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/use_csp_benchmark_integrations.ts b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/use_csp_benchmark_integrations.ts index 53e65d05ce08d..3bef982deb3a9 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/use_csp_benchmark_integrations.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/use_csp_benchmark_integrations.ts @@ -8,7 +8,7 @@ import { useQuery } from 'react-query'; import type { ListResult } from '@kbn/fleet-plugin/common'; import { BENCHMARKS_ROUTE_PATH } from '../../../common/constants'; -import { BenchmarksQuerySchema } from '../../../common/schemas/benchmark'; +import type { BenchmarksQuerySchema } from '../../../common/schemas/benchmark'; import { useKibana } from '../../common/hooks/use_kibana'; import type { Benchmark } from '../../../common/types'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx index e7bc2d5c1b344..0db7392e0b933 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx @@ -16,7 +16,7 @@ import { import { useParams } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n-react'; import { pagePathGetters } from '@kbn/fleet-plugin/public'; -import { cspRuleAssetSavedObjectType } from '../../../common/schemas/csp_rule'; +import { cspRuleAssetSavedObjectType } from '../../../common/constants'; import { extractErrorMessage, isNonNullable } from '../../../common/utils/helpers'; import { RulesTable } from './rules_table'; import { RulesBottomBar } from './rules_bottom_bar'; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules.ts b/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules.ts index 2ce088cd5c29b..8c4012f6c8b45 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules.ts @@ -7,8 +7,11 @@ import { useQuery, useMutation, useQueryClient } from 'react-query'; import { FunctionKeys } from 'utility-types'; import type { SavedObjectsFindOptions, SimpleSavedObject } from '@kbn/core/public'; -import { UPDATE_RULES_CONFIG_ROUTE_PATH } from '../../../common/constants'; -import { cspRuleAssetSavedObjectType, type CspRuleSchema } from '../../../common/schemas/csp_rule'; +import { + UPDATE_RULES_CONFIG_ROUTE_PATH, + cspRuleAssetSavedObjectType, +} from '../../../common/constants'; +import type { CspRuleSchema } from '../../../common/schemas/csp_rule'; import { useKibana } from '../../common/hooks/use_kibana'; import { UPDATE_FAILED } from './translations'; diff --git a/x-pack/plugins/cloud_security_posture/public/test/fixtures/csp_benchmark_integration.ts b/x-pack/plugins/cloud_security_posture/public/test/fixtures/csp_benchmark_integration.ts index 2bbce9840d800..2f05049c4fe15 100644 --- a/x-pack/plugins/cloud_security_posture/public/test/fixtures/csp_benchmark_integration.ts +++ b/x-pack/plugins/cloud_security_posture/public/test/fixtures/csp_benchmark_integration.ts @@ -36,7 +36,13 @@ export const createCspBenchmarkIntegrationFixture = ({ name: chance.sentence(), agents: chance.integer({ min: 0 }), }, + rules = { + all: chance.integer(), + enabled: chance.integer(), + disabled: chance.integer(), + }, }: CreateCspBenchmarkIntegrationFixtureInput = {}): Benchmark => ({ package_policy, agent_policy, + rules, }); diff --git a/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.test.ts index 69e13ab8c5065..82d831c362fbb 100644 --- a/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.test.ts @@ -66,6 +66,24 @@ describe('create CSP rules with post package create callback', () => { ]); }); + it('validate that all rules templates are fetched', async () => { + const mockPackagePolicy = createPackagePolicyMock(); + mockPackagePolicy.package!.name = CLOUD_SECURITY_POSTURE_PACKAGE_NAME; + mockSoClient.find.mockResolvedValueOnce({ + saved_objects: [ + { + type: 'csp-rule-template', + id: 'csp_rule_template-41308bcdaaf665761478bb6f0d745a5c', + attributes: { ...ruleAttributes }, + }, + ], + pit_id: undefined, + } as unknown as SavedObjectsFindResponse); + await onPackagePolicyPostCreateCallback(logger, mockPackagePolicy, mockSoClient); + + expect(mockSoClient.find.mock.calls[0][0]).toMatchObject({ perPage: 10000 }); + }); + it('should not create rules when the package policy is not csp package', async () => { const mockPackagePolicy = createPackagePolicyMock(); mockPackagePolicy.package!.name = 'not_csp_package'; diff --git a/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts index da45a53256bde..9404985ce077f 100644 --- a/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/cloud_security_posture/server/fleet_integration/fleet_integration.ts @@ -17,8 +17,11 @@ import { cloudSecurityPostureRuleTemplateSavedObjectType, CloudSecurityPostureRuleTemplateSchema, } from '../../common/schemas/csp_rule_template'; -import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../../common/constants'; -import { CspRuleSchema, cspRuleAssetSavedObjectType } from '../../common/schemas/csp_rule'; +import { + CLOUD_SECURITY_POSTURE_PACKAGE_NAME, + cspRuleAssetSavedObjectType, +} from '../../common/constants'; +import { CspRuleSchema } from '../../common/schemas/csp_rule'; type ArrayElement = ArrayType extends ReadonlyArray< infer ElementType @@ -46,7 +49,10 @@ export const onPackagePolicyPostCreateCallback = async ( } // Create csp-rules from the generic asset const existingRuleTemplates: SavedObjectsFindResponse = - await savedObjectsClient.find({ type: cloudSecurityPostureRuleTemplateSavedObjectType }); + await savedObjectsClient.find({ + type: cloudSecurityPostureRuleTemplateSavedObjectType, + perPage: 10000, + }); if (existingRuleTemplates.total === 0) { return; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts index 4ed7bdb000344..1bcf5196d1d3d 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts @@ -23,12 +23,13 @@ import { import { defineGetBenchmarksRoute, PACKAGE_POLICY_SAVED_OBJECT_TYPE, - getPackagePolicies, + getCspPackagePolicies, getAgentPolicies, createBenchmarkEntry, + addPackagePolicyCspRules, } from './benchmarks'; -import { SavedObjectsClientContract } from '@kbn/core/server'; +import { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server'; import { createMockAgentPolicyService, createPackagePolicyServiceMock, @@ -221,7 +222,7 @@ describe('benchmarks API', () => { it('should format request by package name', async () => { const mockPackagePolicyService = createPackagePolicyServiceMock(); - await getPackagePolicies(mockSoClient, mockPackagePolicyService, 'myPackage', { + await getCspPackagePolicies(mockSoClient, mockPackagePolicyService, 'myPackage', { page: 1, per_page: 100, sort_order: 'desc', @@ -239,7 +240,7 @@ describe('benchmarks API', () => { it('should build sort request by `sort_field` and default `sort_order`', async () => { const mockAgentPolicyService = createPackagePolicyServiceMock(); - await getPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { + await getCspPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { page: 1, per_page: 100, sort_field: 'package_policy.name', @@ -260,7 +261,7 @@ describe('benchmarks API', () => { it('should build sort request by `sort_field` and asc `sort_order`', async () => { const mockAgentPolicyService = createPackagePolicyServiceMock(); - await getPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { + await getCspPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { page: 1, per_page: 100, sort_field: 'package_policy.name', @@ -282,7 +283,7 @@ describe('benchmarks API', () => { it('should format request by benchmark_name', async () => { const mockAgentPolicyService = createPackagePolicyServiceMock(); - await getPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { + await getCspPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { page: 1, per_page: 100, sort_order: 'desc', @@ -322,6 +323,32 @@ describe('benchmarks API', () => { }); }); + describe('test addPackagePolicyCspRules', () => { + it('should filter enabled rules', async () => { + const packagePolicy = createPackagePolicyMock(); + mockSoClient.find.mockResolvedValueOnce({ + aggregations: { enabled_status: { doc_count: 2 } }, + page: 1, + per_page: 10000, + total: 3, + saved_objects: [ + { + type: 'csp_rule', + id: '0af387d0-c933-11ec-b6c8-4f8afc058cc3', + }, + ], + } as unknown as SavedObjectsFindResponse); + + const cspRulesStatus = await addPackagePolicyCspRules(mockSoClient, packagePolicy); + + expect(cspRulesStatus).toEqual({ + all: 3, + enabled: 2, + disabled: 1, + }); + }); + }); + describe('test createBenchmarkEntry', () => { it('should build benchmark entry agent policy and package policy', async () => { const packagePolicy = createPackagePolicyMock(); @@ -329,9 +356,18 @@ describe('benchmarks API', () => { // @ts-expect-error agentPolicy.agents = 3; - const enrichAgentPolicy = await createBenchmarkEntry(agentPolicy, packagePolicy); + const cspRulesStatus = { + all: 100, + enabled: 52, + disabled: 48, + }; + const enrichAgentPolicy = await createBenchmarkEntry( + agentPolicy, + packagePolicy, + cspRulesStatus + ); - expect(enrichAgentPolicy).toMatchObject({ + expect(enrichAgentPolicy).toEqual({ package_policy: { id: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1', name: 'endpoint-1', @@ -348,6 +384,11 @@ describe('benchmarks API', () => { }, }, agent_policy: { id: 'some-uuid1', name: 'Test Policy', agents: 3 }, + rules: { + all: 100, + disabled: 48, + enabled: 52, + }, }); }); }); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts index a2cb7d06f1f58..d19a86d5b5c27 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts @@ -6,7 +6,7 @@ */ import { uniq, map } from 'lodash'; -import type { SavedObjectsClientContract } from '@kbn/core/server'; +import type { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server'; import { transformError } from '@kbn/securitysolution-es-utils'; import type { PackagePolicyServiceInterface, @@ -19,9 +19,11 @@ import type { AgentPolicy, ListResult, } from '@kbn/fleet-plugin/common'; +import { CspRuleSchema } from '../../../common/schemas/csp_rule'; import { BENCHMARKS_ROUTE_PATH, CLOUD_SECURITY_POSTURE_PACKAGE_NAME, + cspRuleAssetSavedObjectType, } from '../../../common/constants'; import { BENCHMARK_PACKAGE_POLICY_PREFIX, @@ -29,7 +31,7 @@ import { BenchmarksQuerySchema, } from '../../../common/schemas/benchmark'; import { CspAppContext } from '../../plugin'; -import type { Benchmark } from '../../../common/types'; +import type { Benchmark, CspRulesStatus } from '../../../common/types'; import { isNonNullable } from '../../../common/utils/helpers'; import { CspRouter } from '../../types'; @@ -44,7 +46,7 @@ const getPackageNameQuery = (packageName: string, benchmarkFilter?: string): str return kquery; }; -export const getPackagePolicies = ( +export const getCspPackagePolicies = ( soClient: SavedObjectsClientContract, packagePolicyService: PackagePolicyServiceInterface, packageName: string, @@ -94,10 +96,49 @@ const addRunningAgentToAgentPolicy = async ( ) ); }; +export interface RulesStatusAggregation { + enabled_status: { + doc_count: number; + }; +} +export const getCspRulesStatus = ( + soClient: SavedObjectsClientContract, + packagePolicy: PackagePolicy +): Promise> => { + const cspRules = soClient.find({ + type: cspRuleAssetSavedObjectType, + filter: `${cspRuleAssetSavedObjectType}.attributes.package_policy_id: ${packagePolicy.id} AND ${cspRuleAssetSavedObjectType}.attributes.policy_id: ${packagePolicy.policy_id}`, + aggs: { + enabled_status: { + filter: { + term: { + [`${cspRuleAssetSavedObjectType}.attributes.enabled`]: true, + }, + }, + }, + }, + perPage: 0, + }); + return cspRules; +}; + +export const addPackagePolicyCspRules = async ( + soClient: SavedObjectsClientContract, + packagePolicy: PackagePolicy +): Promise => { + const rules = await getCspRulesStatus(soClient, packagePolicy); + const packagePolicyRules = { + all: rules.total, + enabled: rules.aggregations?.enabled_status.doc_count || 0, + disabled: rules.total - (rules.aggregations?.enabled_status.doc_count || 0), + }; + return packagePolicyRules; +}; export const createBenchmarkEntry = ( agentPolicy: GetAgentPoliciesResponseItem, - packagePolicy: PackagePolicy + packagePolicy: PackagePolicy, + cspRulesStatus: CspRulesStatus ): Benchmark => ({ package_policy: { id: packagePolicy.id, @@ -121,26 +162,33 @@ export const createBenchmarkEntry = ( name: agentPolicy.name, agents: agentPolicy.agents, }, + rules: cspRulesStatus, }); const createBenchmarks = ( + soClient: SavedObjectsClientContract, agentPolicies: GetAgentPoliciesResponseItem[], - packagePolicies: PackagePolicy[] -): Benchmark[] => - packagePolicies.flatMap((packagePolicy) => { - return agentPolicies - .map((agentPolicy) => { - const agentPkgPolicies = agentPolicy.package_policies as string[]; - const isExistsOnAgent = agentPkgPolicies.find( - (pkgPolicy) => pkgPolicy === packagePolicy.id - ); - if (isExistsOnAgent) { - return createBenchmarkEntry(agentPolicy, packagePolicy); - } - return; - }) - .filter(isNonNullable); - }); + cspPackagePolicies: PackagePolicy[] +): Promise => { + const cspPackagePoliciesMap = new Map( + cspPackagePolicies.map((packagePolicy) => [packagePolicy.id, packagePolicy]) + ); + return Promise.all( + agentPolicies.flatMap((agentPolicy) => { + const cspPackagesOnAgent = agentPolicy.package_policies + .map((pckPolicyId) => { + if (typeof pckPolicyId === 'string') return cspPackagePoliciesMap.get(pckPolicyId); + }) + .filter(isNonNullable); + const benchmarks = cspPackagesOnAgent.map(async (cspPackage) => { + const cspRulesStatus = await addPackagePolicyCspRules(soClient, cspPackage); + const benchmark = createBenchmarkEntry(agentPolicy, cspPackage, cspRulesStatus); + return benchmark; + }); + return benchmarks; + }) + ); +}; export const defineGetBenchmarksRoute = (router: CspRouter, cspContext: CspAppContext): void => router.get( @@ -165,7 +213,7 @@ export const defineGetBenchmarksRoute = (router: CspRouter, cspContext: CspAppCo throw new Error(`Failed to get Fleet services`); } - const packagePolicies = await getPackagePolicies( + const cspPackagePolicies = await getCspPackagePolicies( soClient, packagePolicyService, CLOUD_SECURITY_POSTURE_PACKAGE_NAME, @@ -174,15 +222,20 @@ export const defineGetBenchmarksRoute = (router: CspRouter, cspContext: CspAppCo const agentPolicies = await getAgentPolicies( soClient, - packagePolicies.items, + cspPackagePolicies.items, agentPolicyService ); + const enrichAgentPolicies = await addRunningAgentToAgentPolicy(agentService, agentPolicies); - const benchmarks = createBenchmarks(enrichAgentPolicies, packagePolicies.items); + const benchmarks = await createBenchmarks( + soClient, + enrichAgentPolicies, + cspPackagePolicies.items + ); return response.ok({ body: { - ...packagePolicies, + ...cspPackagePolicies, items: benchmarks, }, }); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts index 270466d2e3adf..27dcd3cee6703 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts @@ -26,7 +26,9 @@ import { CspAppContext } from '../../plugin'; import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; import { createPackagePolicyServiceMock } from '@kbn/fleet-plugin/server/mocks'; -import { cspRuleAssetSavedObjectType, CspRuleSchema } from '../../../common/schemas/csp_rule'; +import { cspRuleAssetSavedObjectType } from '../../../common/constants'; +import { CspRuleSchema } from '../../../common/schemas/csp_rule'; + import { ElasticsearchClient, KibanaRequest, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts index f0e0e3b8a2bb3..21587394d51e8 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts @@ -20,9 +20,12 @@ import { PackagePolicy, PackagePolicyConfigRecord } from '@kbn/fleet-plugin/comm import { PackagePolicyServiceInterface } from '@kbn/fleet-plugin/server'; import { CspAppContext } from '../../plugin'; import { CspRulesConfigSchema } from '../../../common/schemas/csp_configuration'; -import { CspRuleSchema, cspRuleAssetSavedObjectType } from '../../../common/schemas/csp_rule'; -import { UPDATE_RULES_CONFIG_ROUTE_PATH } from '../../../common/constants'; -import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../../../common/constants'; +import { CspRuleSchema } from '../../../common/schemas/csp_rule'; +import { + CLOUD_SECURITY_POSTURE_PACKAGE_NAME, + UPDATE_RULES_CONFIG_ROUTE_PATH, + cspRuleAssetSavedObjectType, +} from '../../../common/constants'; import { CspRouter } from '../../types'; export const getPackagePolicy = async ( @@ -45,18 +48,17 @@ export const getPackagePolicy = async ( return packagePolicies![0]; }; -export const getCspRules = async ( +export const getCspRules = ( soClient: SavedObjectsClientContract, packagePolicy: PackagePolicy -) => { - const cspRules = await soClient.find({ +): Promise> => { + return soClient.find({ type: cspRuleAssetSavedObjectType, filter: `${cspRuleAssetSavedObjectType}.attributes.package_policy_id: ${packagePolicy.id} AND ${cspRuleAssetSavedObjectType}.attributes.policy_id: ${packagePolicy.policy_id}`, searchFields: ['name'], // TODO: research how to get all rules perPage: 10000, }); - return cspRules; }; export const createRulesConfig = ( diff --git a/x-pack/plugins/cloud_security_posture/server/saved_objects/csp_rule_type.ts b/x-pack/plugins/cloud_security_posture/server/saved_objects/csp_rule_type.ts index a6309f321328f..3afa68fdea228 100644 --- a/x-pack/plugins/cloud_security_posture/server/saved_objects/csp_rule_type.ts +++ b/x-pack/plugins/cloud_security_posture/server/saved_objects/csp_rule_type.ts @@ -7,11 +7,8 @@ import { i18n } from '@kbn/i18n'; import type { SavedObjectsType, SavedObjectsValidationMap } from '@kbn/core/server'; -import { - type CspRuleSchema, - cspRuleSchema, - cspRuleAssetSavedObjectType, -} from '../../common/schemas/csp_rule'; +import { cspRuleAssetSavedObjectType } from '../../common/constants'; +import { type CspRuleSchema, cspRuleSchema } from '../../common/schemas/csp_rule'; const validationMap: SavedObjectsValidationMap = { '1.0.0': cspRuleSchema, @@ -38,6 +35,14 @@ export const ruleAssetSavedObjectMappings: SavedObjectsType['mapp description: { type: 'text', }, + enabled: { + type: 'boolean', + fields: { + keyword: { + type: 'keyword', // sort + }, + }, + }, }, }; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/link_card/link_card.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/link_card/link_card.tsx index ae5c4f0ab9ad9..4d5b5a8154e72 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/link_card/link_card.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/link_card/link_card.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, ReactElement } from 'react'; +import React, { FC } from 'react'; import { EuiIcon, @@ -18,8 +18,8 @@ import { EuiLink, } from '@elastic/eui'; -interface Props { - icon: IconType | ReactElement; +export interface LinkCardProps { + icon: IconType; iconAreaLabel?: string; title: any; description: any; @@ -31,7 +31,7 @@ interface Props { // Component for rendering a card which links to the Create Job page, displaying an // icon, card title, description and link. -export const LinkCard: FC = ({ +export const LinkCard: FC = ({ icon, iconAreaLabel, title, @@ -39,7 +39,7 @@ export const LinkCard: FC = ({ onClick, href, isDisabled, - 'data-test-subj': dateTestSubj, + 'data-test-subj': dataTestSubj, }) => { const linkHrefAndOnClickProps = { ...(href ? { href } : {}), @@ -58,7 +58,7 @@ export const LinkCard: FC = ({ background: 'transparent', outline: 'none', }} - data-test-subj={dateTestSubj} + data-test-subj={dataTestSubj} color="subdued" {...linkHrefAndOnClickProps} > diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/results_links/index.ts b/x-pack/plugins/data_visualizer/public/application/common/components/results_links/index.ts index 24c36f97d7633..4c59725037577 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/results_links/index.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/results_links/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export type { ResultLink } from './results_links'; +export type { ResultLink, GetAdditionalLinks, GetAdditionalLinksParams } from './results_links'; export { ResultsLinks } from './results_links'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/results_links/results_links.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/results_links/results_links.tsx index 57395b506af28..64b0097401ef6 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/results_links/results_links.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/results_links/results_links.tsx @@ -12,10 +12,23 @@ import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; import { TimeRange, RefreshInterval } from '@kbn/data-plugin/public'; import { FindFileStructureResponse } from '@kbn/file-upload-plugin/common'; import type { FileUploadPluginStart } from '@kbn/file-upload-plugin/public'; +import { flatten } from 'lodash'; +import { LinkCardProps } from '../link_card/link_card'; import { useDataVisualizerKibana } from '../../../kibana_context'; +import { isDefined } from '../../util/is_defined'; type LinkType = 'file' | 'index'; +export interface GetAdditionalLinksParams { + dataViewId: string; + dataViewTitle?: string; + globalState?: any; +} + +export type GetAdditionalLinks = Array< + (params: GetAdditionalLinksParams) => Promise +>; + export interface ResultLink { id: string; type: LinkType; @@ -24,7 +37,7 @@ export interface ResultLink { description: string; getUrl(params?: any): Promise; canDisplay(params?: any): Promise; - dataTestSubj?: string; + 'data-test-subj'?: string; } interface Props { @@ -34,7 +47,7 @@ interface Props { timeFieldName?: string; createDataView: boolean; showFilebeatFlyout(): void; - additionalLinks: ResultLink[]; + getAdditionalLinks?: GetAdditionalLinks; } interface GlobalState { @@ -51,7 +64,7 @@ export const ResultsLinks: FC = ({ timeFieldName, createDataView, showFilebeatFlyout, - additionalLinks, + getAdditionalLinks, }) => { const { services: { @@ -70,7 +83,7 @@ export const ResultsLinks: FC = ({ const [discoverLink, setDiscoverLink] = useState(''); const [indexManagementLink, setIndexManagementLink] = useState(''); const [dataViewsManagementLink, setDataViewsManagementLink] = useState(''); - const [generatedLinks, setGeneratedLinks] = useState>({}); + const [asyncHrefCards, setAsyncHrefCards] = useState(); useEffect(() => { let unmounted = false; @@ -93,22 +106,30 @@ export const ResultsLinks: FC = ({ getDiscoverUrl(); - Promise.all( - additionalLinks.map(async ({ canDisplay, getUrl }) => { - if ((await canDisplay({ indexPatternId: dataViewId })) === false) { - return null; - } - return getUrl({ globalState, indexPatternId: dataViewId }); - }) - ).then((urls) => { - const linksById = urls.reduce((acc, url, i) => { - if (url !== null) { - acc[additionalLinks[i].id] = url; - } - return acc; - }, {} as Record); - setGeneratedLinks(linksById); - }); + if (Array.isArray(getAdditionalLinks)) { + Promise.all( + getAdditionalLinks.map(async (asyncCardGetter) => { + const results = await asyncCardGetter({ + dataViewId, + }); + if (Array.isArray(results)) { + return await Promise.all( + results.map(async (c) => ({ + ...c, + canDisplay: await c.canDisplay(), + href: await c.getUrl(), + })) + ); + } + }) + ).then((cards) => { + setAsyncHrefCards( + flatten(cards) + .filter(isDefined) + .filter((d) => d.canDisplay === true) + ); + }); + } if (!unmounted) { setIndexManagementLink( @@ -244,16 +265,15 @@ export const ResultsLinks: FC = ({ onClick={showFilebeatFlyout} />
- {additionalLinks - .filter(({ id }) => generatedLinks[id] !== undefined) - .map((link) => ( + {Array.isArray(asyncHrefCards) && + asyncHrefCards.map((link) => ( } data-test-subj="fileDataVisLink" title={link.title} description={link.description} - href={generatedLinks[link.id]} + href={link.href} /> ))} diff --git a/x-pack/plugins/data_visualizer/public/application/common/util/is_defined.ts b/x-pack/plugins/data_visualizer/public/application/common/util/is_defined.ts new file mode 100644 index 0000000000000..ead91eafc2d4e --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/common/util/is_defined.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 function isDefined(argument: T | undefined | null): argument is T { + return argument !== undefined && argument !== null; +} diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js index 0d3408a77e7c8..9495c599e73d4 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js @@ -369,7 +369,7 @@ export class FileDataVisualizerView extends Component { hideBottomBar={this.hideBottomBar} savedObjectsClient={this.savedObjectsClient} fileUpload={this.props.fileUpload} - resultsLinks={this.props.resultsLinks} + getAdditionalLinks={this.props.getAdditionalLinks} capabilities={this.props.capabilities} /> diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_view/import_view.js b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_view/import_view.js index c5021f930c942..006bd9e3356f7 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_view/import_view.js +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_view/import_view.js @@ -585,7 +585,7 @@ export class ImportView extends Component { timeFieldName={timeFieldName} createDataView={createDataView} showFilebeatFlyout={this.showFilebeatFlyout} - additionalLinks={this.props.resultsLinks ?? []} + getAdditionalLinks={this.props.getAdditionalLinks ?? []} /> {isFilebeatFlyoutVisible && ( diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/file_data_visualizer.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/file_data_visualizer.tsx index 60a67ef8e33d5..0238659b1f348 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/file_data_visualizer.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/file_data_visualizer.tsx @@ -11,14 +11,14 @@ import { getCoreStart, getPluginsStart } from '../../kibana_services'; // @ts-ignore import { FileDataVisualizerView } from './components/file_data_visualizer_view'; -import { ResultLink } from '../common/components/results_links'; +import { GetAdditionalLinks } from '../common/components/results_links'; interface Props { - additionalLinks?: ResultLink[]; + getAdditionalLinks?: GetAdditionalLinks; } export type FileDataVisualizerSpec = typeof FileDataVisualizer; -export const FileDataVisualizer: FC = ({ additionalLinks }) => { +export const FileDataVisualizer: FC = ({ getAdditionalLinks }) => { const coreStart = getCoreStart(); const { data, maps, embeddable, discover, share, security, fileUpload, cloud } = getPluginsStart(); @@ -45,7 +45,7 @@ export const FileDataVisualizer: FC = ({ additionalLinks }) => { savedObjectsClient={coreStart.savedObjects.client} http={coreStart.http} fileUpload={fileUpload} - resultsLinks={additionalLinks} + getAdditionalLinks={getAdditionalLinks} capabilities={coreStart.application.capabilities} /> diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/actions_panel/actions_panel.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/actions_panel/actions_panel.tsx index c3bb0eef960ba..5d055ed993575 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/actions_panel/actions_panel.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/actions_panel/actions_panel.tsx @@ -11,28 +11,31 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiTitle } from '@elastic/eui'; import { DataView } from '@kbn/data-views-plugin/public'; +import { flatten } from 'lodash'; +import { LinkCardProps } from '../../../common/components/link_card/link_card'; import { useDataVisualizerKibana } from '../../../kibana_context'; import { useUrlState } from '../../../common/util/url_state'; import { LinkCard } from '../../../common/components/link_card'; -import { ResultLink } from '../../../common/components/results_links'; +import { GetAdditionalLinks } from '../../../common/components/results_links'; +import { isDefined } from '../../../common/util/is_defined'; interface Props { dataView: DataView; searchString?: string | { [key: string]: any }; searchQueryLanguage?: string; - additionalLinks: ResultLink[]; + getAdditionalLinks?: GetAdditionalLinks; } export const ActionsPanel: FC = ({ dataView, searchString, searchQueryLanguage, - additionalLinks, + getAdditionalLinks, }) => { const [globalState] = useUrlState('_g'); const [discoverLink, setDiscoverLink] = useState(''); - const [generatedLinks, setGeneratedLinks] = useState>({}); + const [asyncHrefCards, setAsyncHrefCards] = useState(); const { services: { @@ -46,6 +49,7 @@ export const ActionsPanel: FC = ({ let unmounted = false; const indexPatternId = dataView.id; + const indexPatternTitle = dataView.title; const getDiscoverUrl = async (): Promise => { const isDiscoverAvailable = capabilities.discover?.show ?? false; if (!isDiscoverAvailable) return; @@ -68,24 +72,33 @@ export const ActionsPanel: FC = ({ setDiscoverLink(discoverUrl); }; - Promise.all( - additionalLinks.map(async ({ canDisplay, getUrl }) => { - if ((await canDisplay({ indexPatternId })) === false) { - return null; - } - return getUrl({ globalState, indexPatternId }); - }) - ).then((urls) => { - const linksById = urls.reduce((acc, url, i) => { - if (url !== null) { - acc[additionalLinks[i].id] = url; - } - return acc; - }, {} as Record); - setGeneratedLinks(linksById); - }); - + if (Array.isArray(getAdditionalLinks) && indexPatternId !== undefined) { + Promise.all( + getAdditionalLinks.map(async (asyncCardGetter) => { + const results = await asyncCardGetter({ + dataViewId: indexPatternId, + dataViewTitle: indexPatternTitle, + }); + if (Array.isArray(results)) { + return await Promise.all( + results.map(async (c) => ({ + ...c, + canDisplay: await c.canDisplay(), + href: await c.getUrl(), + })) + ); + } + }) + ).then((cards) => { + setAsyncHrefCards( + flatten(cards) + .filter(isDefined) + .filter((d) => d.canDisplay === true) + ); + }); + } getDiscoverUrl(); + return () => { unmounted = true; }; @@ -96,8 +109,8 @@ export const ActionsPanel: FC = ({ globalState, capabilities, discover, - additionalLinks, data.query, + getAdditionalLinks, ]); // Note we use display:none for the DataRecognizer section as it needs to be @@ -105,20 +118,6 @@ export const ActionsPanel: FC = ({ // controls whether the recognizer section is ultimately displayed. return (
- {additionalLinks - .filter(({ id }) => generatedLinks[id] !== undefined) - .map((link) => ( - <> - - - - ))} {discoverLink && ( <> @@ -147,8 +146,23 @@ export const ActionsPanel: FC = ({ } data-test-subj="dataVisualizerViewInDiscoverCard" /> + )} + + {Array.isArray(asyncHrefCards) && + asyncHrefCards.map((link) => ( + <> + + + + ))}
); }; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx index 9ce1fb4f552e5..d892b5f159434 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx @@ -48,7 +48,7 @@ import { DatePickerWrapper } from '../../../common/components/date_picker_wrappe import { HelpMenu } from '../../../common/components/help_menu'; import { createMergedEsQuery } from '../../utils/saved_search_utils'; import { DataVisualizerDataViewManagement } from '../data_view_management'; -import { ResultLink } from '../../../common/components/results_links'; +import { GetAdditionalLinks } from '../../../common/components/results_links'; import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data'; import { DataVisualizerGridInput } from '../../embeddables/grid_embeddable/grid_embeddable'; import './_index.scss'; @@ -110,7 +110,7 @@ export interface IndexDataVisualizerViewProps { currentDataView: DataView; currentSavedSearch: SavedSearchSavedObject | null; currentSessionId?: string; - additionalLinks?: ResultLink[]; + getAdditionalLinks?: GetAdditionalLinks; } const restorableDefaults = getDefaultDataVisualizerListState(); @@ -129,7 +129,7 @@ export const IndexDataVisualizerView: FC = (dataVi dataVisualizerProps.currentSavedSearch ); - const { currentDataView, additionalLinks, currentSessionId } = dataVisualizerProps; + const { currentDataView, currentSessionId, getAdditionalLinks } = dataVisualizerProps; useEffect(() => { if (dataVisualizerProps?.currentSavedSearch !== undefined) { @@ -487,7 +487,7 @@ export const IndexDataVisualizerView: FC = (dataVi dataView={currentDataView} searchQueryLanguage={searchQueryLanguage} searchString={searchString} - additionalLinks={additionalLinks ?? []} + getAdditionalLinks={getAdditionalLinks} /> diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.scss b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.scss index 6f274921d5ebf..6b0624fae2757 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.scss +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.scss @@ -3,6 +3,10 @@ padding: $euiSizeS; } +.dvSearchPanel__container { + align-items: baseline; +} + @include euiBreakpoint('xs', 's', 'm', 'l') { .dvSearchPanel__container { flex-direction: column; @@ -13,8 +17,4 @@ .dvSearchPanel__controls { padding: 0; } - // prevent margin -16 which scrunches the filter bar - .globalFilterGroup__wrapper-isVisible { - margin: 0 !important; - } } diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx index e7ac50c906660..7d218d98afa39 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/search_panel/search_panel.tsx @@ -120,7 +120,6 @@ export const SearchPanel: FC = ({ return ( ; - additionalLinks: ResultLink[]; + getAdditionalLinks?: GetAdditionalLinks; } +export type IndexDataVisualizerSpec = typeof IndexDataVisualizer; export const getLocatorParams = (params: { dataViewId?: string; @@ -73,7 +72,7 @@ export const getLocatorParams = (params: { export const DataVisualizerUrlStateContextProvider: FC< DataVisualizerUrlStateContextProviderProps -> = ({ IndexDataVisualizerComponent, additionalLinks }) => { +> = ({ IndexDataVisualizerComponent, getAdditionalLinks }) => { const { services } = useDataVisualizerKibana(); const { data: { dataViews, search }, @@ -247,8 +246,8 @@ export const DataVisualizerUrlStateContextProvider: FC< ) : (
@@ -257,7 +256,9 @@ export const DataVisualizerUrlStateContextProvider: FC< ); }; -export const IndexDataVisualizer: FC<{ additionalLinks: ResultLink[] }> = ({ additionalLinks }) => { +export const IndexDataVisualizer: FC<{ + getAdditionalLinks?: GetAdditionalLinks; +}> = ({ getAdditionalLinks }) => { const coreStart = getCoreStart(); const { data, @@ -294,7 +295,7 @@ export const IndexDataVisualizer: FC<{ additionalLinks: ResultLink[] }> = ({ add diff --git a/x-pack/plugins/data_visualizer/public/index.ts b/x-pack/plugins/data_visualizer/public/index.ts index 4a579f4e3abcc..7c4208106cb75 100644 --- a/x-pack/plugins/data_visualizer/public/index.ts +++ b/x-pack/plugins/data_visualizer/public/index.ts @@ -18,4 +18,8 @@ export type { IndexDataVisualizerSpec, IndexDataVisualizerViewProps, } from './application'; -export type { ResultLink } from './application/common/components/results_links'; +export type { + GetAdditionalLinksParams, + ResultLink, + GetAdditionalLinks, +} from './application/common/components/results_links'; diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts index 28d9fe363ff0f..3da63a63828ae 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts @@ -196,6 +196,17 @@ describe('checkAccess', () => { hasWorkplaceSearchAccess: false, }); }); + + it('falls back to no access if response error', async () => { + (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({ + responseStatus: 500, + responseStatusText: 'failed', + })); + expect(await checkAccess(mockDependencies)).toEqual({ + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: false, + }); + }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.ts index a77415f2c2f12..444fa9d4fdb29 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.ts @@ -98,6 +98,6 @@ export const checkAccess = async ({ // When enterpriseSearch.host is defined in kibana.yml, // make a HTTP call which returns product access - const { access } = (await callEnterpriseSearchConfigAPI({ request, config, log })) || {}; - return access || DENY_ALL_PLUGINS; + const response = (await callEnterpriseSearchConfigAPI({ request, config, log })) || {}; + return 'access' in response ? response.access || DENY_ALL_PLUGINS : DENY_ALL_PLUGINS; }; diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index 5f8b261f82f16..ad55b41c02cee 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -207,8 +207,19 @@ describe('callEnterpriseSearchConfigAPI', () => { (fetch as unknown as jest.Mock).mockReturnValueOnce(Promise.resolve('Bad Data')); expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({}); expect(mockDependencies.log.error).toHaveBeenCalledWith( - 'Could not perform access check to Enterprise Search: TypeError: response.json is not a function' + 'Could not perform access check to Enterprise Search: 500' + ); + + (fetch as unknown as jest.Mock).mockReturnValueOnce( + Promise.resolve( + new Response('{}', { + status: 500, + statusText: 'I failed', + }) + ) ); + const expected = { responseStatus: 500, responseStatusText: 'I failed' }; + expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual(expected); }); it('handles timeouts', async () => { diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index 7e27480426525..361a5613ab67e 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -26,6 +26,10 @@ interface Params { interface Return extends InitialAppData { publicUrl?: string; } +interface ResponseError { + responseStatus: number; + responseStatusText: string; +} /** * Calls an internal Enterprise Search API endpoint which returns @@ -38,7 +42,7 @@ export const callEnterpriseSearchConfigAPI = async ({ config, log, request, -}: Params): Promise => { +}: Params): Promise => { if (!config.host) return {}; const TIMEOUT_WARNING = `Enterprise Search access check took over ${config.accessCheckTimeoutWarning}ms. Please ensure your Enterprise Search server is responding normally and not adversely impacting Kibana load speeds.`; @@ -63,6 +67,14 @@ export const callEnterpriseSearchConfigAPI = async ({ }; const response = await fetch(enterpriseSearchUrl, options); + + if (!response.ok) { + return { + responseStatus: response.status, + responseStatusText: response.statusText, + }; + } + const data = await response.json(); warnMismatchedVersions(data?.version?.number, log); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts index 95ab8e3c3a5a9..5be5bf8cc0373 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts @@ -17,7 +17,12 @@ export function registerConfigDataRoute({ router, config, log }: RouteDependenci async (context, request, response) => { const data = await callEnterpriseSearchConfigAPI({ request, config, log }); - if (!Object.keys(data).length) { + if ('responseStatus' in data) { + return response.customError({ + statusCode: data.responseStatus, + body: 'Error fetching data from Enterprise Search', + }); + } else if (!Object.keys(data).length) { return response.customError({ statusCode: 502, body: 'Error fetching data from Enterprise Search', diff --git a/x-pack/plugins/fleet/common/constants/data_streams.ts b/x-pack/plugins/fleet/common/constants/data_streams.ts new file mode 100644 index 0000000000000..bb880af9b3df8 --- /dev/null +++ b/x-pack/plugins/fleet/common/constants/data_streams.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 { schema } from '@kbn/config-schema'; + +export const GetDataStreamsListRequestSchema = { + params: schema.object({ + use_terms_enum: schema.boolean({ defaultValue: false }), + }), +}; diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index 888850b73b15e..07f6fa048dc42 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -16,6 +16,7 @@ export const FLEET_ENDPOINT_PACKAGE = 'endpoint'; export const FLEET_APM_PACKAGE = 'apm'; export const FLEET_SYNTHETICS_PACKAGE = 'synthetics'; export const FLEET_KUBERNETES_PACKAGE = 'kubernetes'; +export const FLEET_CLOUD_SECURITY_POSTURE_PACKAGE = 'cloud_security_posture'; export const FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID = 'elastic_agent-f47f18cc-9c7d-4278-b2ea-a6dee816d395'; @@ -58,5 +59,4 @@ export const installationStatuses = { Installing: 'installing', InstallFailed: 'install_failed', NotInstalled: 'not_installed', - InstalledBundled: 'installed_bundled', } as const; diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 5217a6232a18c..2359b979d0a17 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -45,11 +45,7 @@ export interface DefaultPackagesInstallationError { export type InstallType = 'reinstall' | 'reupdate' | 'rollback' | 'update' | 'install' | 'unknown'; export type InstallSource = 'registry' | 'upload' | 'bundled'; -export type EpmPackageInstallStatus = - | 'installed' - | 'installing' - | 'install_failed' - | 'installed_bundled'; +export type EpmPackageInstallStatus = 'installed' | 'installing' | 'install_failed'; export type DetailViewPanelName = 'overview' | 'policies' | 'assets' | 'settings' | 'custom'; export type ServiceName = 'kibana' | 'elasticsearch'; @@ -431,8 +427,7 @@ export type Installable = | InstalledRegistry | Installing | NotInstalled - | InstallFailed - | InstalledBundled; + | InstallFailed; export type InstallStatusExcluded = T & { status: undefined; @@ -443,10 +438,6 @@ export type InstalledRegistry = T & { savedObject: SavedObject; }; -export type InstalledBundled = T & { - status: InstallationStatus['InstalledBundled']; -}; - export type Installing = T & { status: InstallationStatus['Installing']; savedObject: SavedObject; diff --git a/x-pack/plugins/fleet/cypress/README.md b/x-pack/plugins/fleet/cypress/README.md index e9bb299ca905e..94de6b38c47ec 100644 --- a/x-pack/plugins/fleet/cypress/README.md +++ b/x-pack/plugins/fleet/cypress/README.md @@ -113,13 +113,13 @@ We use es_archiver to manage the data that our Cypress tests need. 3. When you are sure that you have all the data you need run the following command from: `x-pack/plugins/fleet` ```sh -node ../../../scripts/es_archiver save --dir ../../test/fleet_cypress/es_archives --config ../../../test/functional/config.js --es-url http://:@: +node ../../../scripts/es_archiver save --dir ../../test/fleet_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http://:@: ``` Example: ```sh -node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/fleet_cypress/es_archives --config ../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220 +node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/fleet_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http://elastic:changeme@localhost:9220 ``` Note that the command will create the folder if it does not exist. diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx index 159de12ff578c..886ce736b3625 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/hooks.tsx @@ -9,11 +9,13 @@ import { i18n } from '@kbn/i18n'; import type { PackagePolicy, AgentPolicy } from '../../types'; import { sendGetOneAgentPolicy, useStartServices } from '../../hooks'; - -import { FLEET_KUBERNETES_PACKAGE } from '../../../common'; +import { FLEET_KUBERNETES_PACKAGE, FLEET_CLOUD_SECURITY_POSTURE_PACKAGE } from '../../../common'; import type { K8sMode } from './types'; +// Packages that requires custom elastic-agent manifest +const K8S_PACKAGES = new Set([FLEET_KUBERNETES_PACKAGE, FLEET_CLOUD_SECURITY_POSTURE_PACKAGE]); + export function useAgentPolicyWithPackagePolicies(policyId?: string) { const [agentPolicyWithPackagePolicies, setAgentPolicy] = useState(null); const core = useStartServices(); @@ -64,4 +66,4 @@ export function useIsK8sPolicy(agentPolicy?: AgentPolicy) { return { isK8s }; } -const isK8sPackage = (pkg: PackagePolicy) => pkg.package?.name === FLEET_KUBERNETES_PACKAGE; +const isK8sPackage = (pkg: PackagePolicy) => K8S_PACKAGES.has(pkg.package?.name as string); diff --git a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts index 2d01344a930aa..ad3b356828d72 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts @@ -6,13 +6,16 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { keyBy, keys, merge } from 'lodash'; -import type { RequestHandler } from '@kbn/core/server'; +import type { RequestHandler, ElasticsearchClient } from '@kbn/core/server'; + +import type { TypeOf } from '@kbn/config-schema'; import type { DataStream } from '../../types'; import { KibanaSavedObjectType } from '../../../common'; import type { GetDataStreamsResponse } from '../../../common'; import { getPackageSavedObjects } from '../../services/epm/packages/get'; import { defaultIngestErrorHandler } from '../../errors'; +import type { GetDataStreamsListRequestSchema } from '../../../common/constants/data_streams'; const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*,traces-*-*,synthetics-*-*'; @@ -37,11 +40,139 @@ interface ESDataStreamInfo { hidden: boolean; } +async function getMetadataFromTermsEnum({ + dataStreamName, + esClient, +}: { + dataStreamName: string; + esClient: ElasticsearchClient; +}) { + const [maxEventIngestedResponse, namespaceResponse, datasetResponse, typeResponse] = + await Promise.all([ + esClient.search({ + size: 1, + index: dataStreamName, + sort: { + // @ts-expect-error Type '{ 'event.ingested': string; }' is not assignable to type 'string | string[] | undefined'. + 'event.ingested': 'desc', + }, + _source: false, + fields: ['event.ingested'], + }), + esClient.termsEnum({ + index: dataStreamName, + field: 'data_stream.namespace', + }), + esClient.termsEnum({ + index: dataStreamName, + field: 'data_stream.dataset', + }), + esClient.termsEnum({ + index: dataStreamName, + field: 'data_stream.type', + }), + ]); + + const maxIngested = new Date( + maxEventIngestedResponse.hits.hits[0]?.fields!['event.ingested'] + ).getTime(); + + const namespace = namespaceResponse.terms[0] ?? ''; + const dataset = datasetResponse.terms[0] ?? ''; + const type = typeResponse.terms[0] ?? ''; + + return { + maxIngested, + namespace, + dataset, + type, + }; +} + +async function getMetadataFromAggregations({ + dataStreamName, + esClient, +}: { + dataStreamName: string; + esClient: ElasticsearchClient; +}) { + // Query backing indices to extract data stream dataset, namespace, and type values + const { aggregations: dataStreamAggs } = await esClient.search({ + index: dataStreamName, + body: { + size: 0, + query: { + bool: { + filter: [ + { + exists: { + field: 'data_stream.namespace', + }, + }, + { + exists: { + field: 'data_stream.dataset', + }, + }, + ], + }, + }, + aggs: { + maxIngestedTimestamp: { + max: { + field: 'event.ingested', + }, + }, + dataset: { + terms: { + field: 'data_stream.dataset', + size: 1, + }, + }, + namespace: { + terms: { + field: 'data_stream.namespace', + size: 1, + }, + }, + type: { + terms: { + field: 'data_stream.type', + size: 1, + }, + }, + }, + }, + }); + + const { maxIngestedTimestamp } = dataStreamAggs as Record< + string, + estypes.AggregationsRateAggregate + >; + const { dataset, namespace, type } = dataStreamAggs as Record< + string, + estypes.AggregationsMultiBucketAggregateBase<{ key?: string; value?: number }> + >; + + const maxIngested = maxIngestedTimestamp?.value; + + return { + maxIngested, + dataset: (dataset.buckets as Array<{ key?: string; value?: number }>)[0]?.key || '', + namespace: (namespace.buckets as Array<{ key?: string; value?: number }>)[0]?.key || '', + type: (type.buckets as Array<{ key?: string; value?: number }>)[0]?.key || '', + }; +} + export const getListHandler: RequestHandler = async (context, request, response) => { // Query datastreams as the current user as the Kibana internal user may not have all the required permission const { savedObjects, elasticsearch } = await context.core; const esClient = elasticsearch.client.asCurrentUser; + const { use_terms_enum: useTermsEnum } = request.params as TypeOf< + typeof GetDataStreamsListRequestSchema['params'] + >; + const body: GetDataStreamsResponse = { data_streams: [], }; @@ -127,75 +258,18 @@ export const getListHandler: RequestHandler = async (context, request, response) dashboards: [], }; - // Query backing indices to extract data stream dataset, namespace, and type values - const { aggregations: dataStreamAggs } = await esClient.search({ - index: dataStream.name, - body: { - size: 0, - query: { - bool: { - filter: [ - { - exists: { - field: 'data_stream.namespace', - }, - }, - { - exists: { - field: 'data_stream.dataset', - }, - }, - ], - }, - }, - aggs: { - maxIngestedTimestamp: { - max: { - field: 'event.ingested', - }, - }, - dataset: { - terms: { - field: 'data_stream.dataset', - size: 1, - }, - }, - namespace: { - terms: { - field: 'data_stream.namespace', - size: 1, - }, - }, - type: { - terms: { - field: 'data_stream.type', - size: 1, - }, - }, - }, - }, - }); - - const { maxIngestedTimestamp } = dataStreamAggs as Record< - string, - estypes.AggregationsRateAggregate - >; - const { dataset, namespace, type } = dataStreamAggs as Record< - string, - estypes.AggregationsMultiBucketAggregateBase<{ key?: string; value?: number }> - >; + const { maxIngested, namespace, dataset, type } = useTermsEnum + ? await getMetadataFromTermsEnum({ dataStreamName: dataStream.name, esClient }) + : await getMetadataFromAggregations({ dataStreamName: dataStream.name, esClient }); // some integrations e.g custom logs don't have event.ingested - if (maxIngestedTimestamp?.value) { - dataStreamResponse.last_activity_ms = maxIngestedTimestamp?.value; + if (maxIngested) { + dataStreamResponse.last_activity_ms = maxIngested; } - dataStreamResponse.dataset = - (dataset.buckets as Array<{ key?: string; value?: number }>)[0]?.key || ''; - dataStreamResponse.namespace = - (namespace.buckets as Array<{ key?: string; value?: number }>)[0]?.key || ''; - dataStreamResponse.type = - (type.buckets as Array<{ key?: string; value?: number }>)[0]?.key || ''; + dataStreamResponse.dataset = dataset; + dataStreamResponse.namespace = namespace; + dataStreamResponse.type = type; // Find package saved object const pkgName = dataStreamResponse.package; diff --git a/x-pack/plugins/fleet/server/routes/data_streams/index.ts b/x-pack/plugins/fleet/server/routes/data_streams/index.ts index ddefc537ba207..d7491d87e2a17 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/index.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { GetDataStreamsListRequestSchema } from '../../../common/constants/data_streams'; import { DATA_STREAM_API_ROUTES } from '../../constants'; import type { FleetAuthzRouter } from '../security'; @@ -15,7 +16,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => { router.get( { path: DATA_STREAM_API_ROUTES.LIST_PATTERN, - validate: false, + validate: GetDataStreamsListRequestSchema, fleetAuthz: { fleet: { all: true }, }, diff --git a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts index 77fe33df4ca48..74cf93c1c6bcb 100644 --- a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts +++ b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts @@ -5,8 +5,7 @@ * 2.0. */ -export const elasticAgentStandaloneManifest = ` ---- +export const elasticAgentStandaloneManifest = `--- apiVersion: apps/v1 kind: DaemonSet metadata: @@ -66,6 +65,11 @@ spec: - name: proc mountPath: /hostfs/proc readOnly: true + - name: etc-kubernetes + mountPath: /hostfs/etc/kubernetes + - name: var-lib + mountPath: /hostfs/var/lib + readOnly: true - name: cgroup mountPath: /hostfs/sys/fs/cgroup readOnly: true @@ -75,6 +79,15 @@ spec: - name: varlog mountPath: /var/log readOnly: true + - name: passwd + mountPath: /hostfs/etc/passwd + readOnly: true + - name: group + mountPath: /hostfs/etc/group + readOnly: true + - name: systemd + mountPath: /hostfs/etc/systemd + readOnly: true volumes: - name: datastreams configMap: @@ -83,6 +96,18 @@ spec: - name: proc hostPath: path: /proc + - name: etc-kubernetes + hostPath: + path: /etc/kubernetes + - name: var-lib + hostPath: + path: /var/lib + - name: passwd + hostPath: + path: /etc/passwd + - name: group + hostPath: + path: /etc/group - name: cgroup hostPath: path: /sys/fs/cgroup @@ -92,6 +117,9 @@ spec: - name: varlog hostPath: path: /var/log + - name: systemd + hostPath: + path: /etc/systemd --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -149,6 +177,7 @@ rules: - pods - services - configmaps + - serviceaccounts verbs: ["get", "list", "watch"] # Enable this rule only if planing to use kubernetes_secrets provider #- apiGroups: [""] @@ -181,6 +210,23 @@ rules: - "/metrics" verbs: - get + # required for cloudbeat + - apiGroups: ["rbac.authorization.k8s.io"] + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: ["get", "list", "watch"] + - apiGroups: ["networking.k8s.io"] + resources: + - ingressclasses + - ingresses + verbs: ["get", "list", "watch"] + - apiGroups: ["policy"] + resources: + - podsecuritypolicies + verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -222,7 +268,8 @@ metadata: --- `; -export const elasticAgentManagedManifest = `apiVersion: apps/v1 +export const elasticAgentManagedManifest = `--- +apiVersion: apps/v1 kind: DaemonSet metadata: name: elastic-agent @@ -285,6 +332,11 @@ spec: - name: proc mountPath: /hostfs/proc readOnly: true + - name: etc-kubernetes + mountPath: /hostfs/etc/kubernetes + - name: var-lib + mountPath: /hostfs/var/lib + readOnly: true - name: cgroup mountPath: /hostfs/sys/fs/cgroup readOnly: true @@ -294,10 +346,31 @@ spec: - name: varlog mountPath: /var/log readOnly: true + - name: passwd + mountPath: /hostfs/etc/passwd + readOnly: true + - name: group + mountPath: /hostfs/etc/group + readOnly: true + - name: systemd + mountPath: /hostfs/etc/systemd + readOnly: true volumes: - name: proc hostPath: path: /proc + - name: etc-kubernetes + hostPath: + path: /etc/kubernetes + - name: var-lib + hostPath: + path: /var/lib + - name: passwd + hostPath: + path: /etc/passwd + - name: group + hostPath: + path: /etc/group - name: cgroup hostPath: path: /sys/fs/cgroup @@ -307,6 +380,9 @@ spec: - name: varlog hostPath: path: /var/log + - name: systemd + hostPath: + path: /etc/systemd --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -364,6 +440,7 @@ rules: - pods - services - configmaps + - serviceaccounts verbs: ["get", "list", "watch"] # Enable this rule only if planing to use kubernetes_secrets provider #- apiGroups: [""] @@ -396,6 +473,23 @@ rules: - "/metrics" verbs: - get + # required for cloudbeat + - apiGroups: ["rbac.authorization.k8s.io"] + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: ["get", "list", "watch"] + - apiGroups: ["networking.k8s.io"] + resources: + - ingressclasses + - ingresses + verbs: ["get", "list", "watch"] + - apiGroups: ["policy"] + resources: + - podsecuritypolicies + verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -435,5 +529,4 @@ metadata: labels: k8s-app: elastic-agent --- - `; diff --git a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts index cb9f5650550e8..2c313f2f2761d 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts @@ -123,7 +123,7 @@ export async function saveArchiveEntries(opts: { }) ); - const results = await savedObjectsClient.bulkCreate(bulkBody); + const results = await savedObjectsClient.bulkCreate(bulkBody, { refresh: false }); return results; } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts index 4f18966a61307..c6be2dfedb1df 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/install.ts @@ -13,14 +13,13 @@ import type { InstallablePackage, RegistryDataStream, } from '../../../../../common/types/models'; -import { getInstallation } from '../../packages'; -import { saveInstalledEsRefs } from '../../packages/install'; +import { updateEsAssetReferences } from '../../packages/install'; import { getAsset } from '../transform/common'; import { getESAssetMetadata } from '../meta'; import { retryTransientEsErrors } from '../retry'; -import { deleteIlmRefs, deleteIlms } from './remove'; +import { deleteIlms } from './remove'; interface IlmInstallation { installationName: string; @@ -37,24 +36,39 @@ export const installIlmForDataStream = async ( paths: string[], esClient: ElasticsearchClient, savedObjectsClient: SavedObjectsClientContract, - logger: Logger + logger: Logger, + esReferences: EsAssetReference[] ) => { - const installation = await getInstallation({ savedObjectsClient, pkgName: registryPackage.name }); - let previousInstalledIlmEsAssets: EsAssetReference[] = []; - if (installation) { - previousInstalledIlmEsAssets = installation.installed_es.filter( - ({ type, id }) => type === ElasticsearchAssetType.dataStreamIlmPolicy - ); - } + const previousInstalledIlmEsAssets = esReferences.filter( + ({ type }) => type === ElasticsearchAssetType.dataStreamIlmPolicy + ); // delete all previous ilm await deleteIlms( esClient, previousInstalledIlmEsAssets.map((asset) => asset.id) ); + + if (previousInstalledIlmEsAssets.length > 0) { + // remove the saved object reference + esReferences = await updateEsAssetReferences( + savedObjectsClient, + registryPackage.name, + esReferences, + { + assetsToRemove: previousInstalledIlmEsAssets, + } + ); + } + // install the latest dataset const dataStreams = registryPackage.data_streams; - if (!dataStreams?.length) return []; + if (!dataStreams?.length) + return { + installedIlms: [], + esReferences, + }; + const dataStreamIlmPaths = paths.filter((path) => isDataStreamIlm(path)); let installedIlms: EsAssetReference[] = []; if (dataStreamIlmPaths.length > 0) { @@ -77,12 +91,17 @@ export const installIlmForDataStream = async ( return acc; }, []); - await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, ilmRefs); + esReferences = await updateEsAssetReferences( + savedObjectsClient, + registryPackage.name, + esReferences, + { assetsToAdd: ilmRefs } + ); const ilmInstallations: IlmInstallation[] = ilmPathDatasets.map( (ilmPathDataset: IlmPathDataset) => { const content = JSON.parse(getAsset(ilmPathDataset.path).toString('utf-8')); - content.policy._meta = getESAssetMetadata({ packageName: installation?.name }); + content.policy._meta = getESAssetMetadata({ packageName: registryPackage.name }); return { installationName: getIlmNameForInstallation(ilmPathDataset), @@ -98,22 +117,7 @@ export const installIlmForDataStream = async ( installedIlms = await Promise.all(installationPromises).then((results) => results.flat()); } - if (previousInstalledIlmEsAssets.length > 0) { - const currentInstallation = await getInstallation({ - savedObjectsClient, - pkgName: registryPackage.name, - }); - - // remove the saved object reference - await deleteIlmRefs( - savedObjectsClient, - currentInstallation?.installed_es || [], - registryPackage.name, - previousInstalledIlmEsAssets.map((asset) => asset.id), - installedIlms.map((installed) => installed.id) - ); - } - return installedIlms; + return { installedIlms, esReferences }; }; async function handleIlmInstall({ diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/remove.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/remove.ts index 1d98a9339c907..331088d195d0b 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/datastream_ilm/remove.ts @@ -5,11 +5,7 @@ * 2.0. */ -import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; - -import { ElasticsearchAssetType } from '../../../../types'; -import type { EsAssetReference } from '../../../../types'; -import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common/constants'; +import type { ElasticsearchClient } from '@kbn/core/server'; export const deleteIlms = async (esClient: ElasticsearchClient, ilmPolicyIds: string[]) => { await Promise.all( @@ -26,24 +22,3 @@ export const deleteIlms = async (esClient: ElasticsearchClient, ilmPolicyIds: st }) ); }; - -export const deleteIlmRefs = async ( - savedObjectsClient: SavedObjectsClientContract, - installedEsAssets: EsAssetReference[], - pkgName: string, - installedEsIdToRemove: string[], - currentInstalledEsIlmIds: string[] -) => { - const seen = new Set(); - const filteredAssets = installedEsAssets.filter(({ type, id }) => { - if (type !== ElasticsearchAssetType.dataStreamIlmPolicy) return true; - const add = - (currentInstalledEsIlmIds.includes(id) || !installedEsIdToRemove.includes(id)) && - !seen.has(id); - seen.add(id); - return add; - }); - return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_es: filteredAssets, - }); -}; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts index 9b64ec89507dc..3aa86b526addd 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ilm/install.ts @@ -5,12 +5,13 @@ * 2.0. */ -import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; -import type { InstallablePackage } from '../../../../types'; +import type { EsAssetReference, InstallablePackage } from '../../../../types'; import { ElasticsearchAssetType } from '../../../../types'; import { getAsset, getPathParts } from '../../archive'; +import { updateEsAssetReferences } from '../../packages/install'; import { getESAssetMetadata } from '../meta'; import { retryTransientEsErrors } from '../retry'; @@ -18,25 +19,40 @@ export async function installILMPolicy( packageInfo: InstallablePackage, paths: string[], esClient: ElasticsearchClient, - logger: Logger -) { + savedObjectsClient: SavedObjectsClientContract, + logger: Logger, + esReferences: EsAssetReference[] +): Promise { const ilmPaths = paths.filter((path) => isILMPolicy(path)); - if (!ilmPaths.length) return; - await Promise.all( - ilmPaths.map(async (path) => { - const body = JSON.parse(getAsset(path).toString('utf-8')); + if (!ilmPaths.length) return esReferences; + + const ilmPolicies = ilmPaths.map((path) => { + const body = JSON.parse(getAsset(path).toString('utf-8')); + + body.policy._meta = getESAssetMetadata({ packageName: packageInfo.name }); + + const { file } = getPathParts(path); + const name = file.substr(0, file.lastIndexOf('.')); - body.policy._meta = getESAssetMetadata({ packageName: packageInfo.name }); + return { name, body }; + }); - const { file } = getPathParts(path); - const name = file.substr(0, file.lastIndexOf('.')); + esReferences = await updateEsAssetReferences(savedObjectsClient, packageInfo.name, esReferences, { + assetsToAdd: ilmPolicies.map((policy) => ({ + type: ElasticsearchAssetType.ilmPolicy, + id: policy.name, + })), + }); + + await Promise.all( + ilmPolicies.map(async (policy) => { try { await retryTransientEsErrors( () => esClient.transport.request({ method: 'PUT', - path: '/_ilm/policy/' + name, - body, + path: '/_ilm/policy/' + policy.name, + body: policy.body, }), { logger } ); @@ -45,6 +61,8 @@ export async function installILMPolicy( } }) ); + + return esReferences; } const isILMPolicy = (path: string) => { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts index 574534290214a..5f093a19157f9 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts @@ -5,6 +5,6 @@ * 2.0. */ -export { installPipelines, isTopLevelPipeline } from './install'; +export { prepareToInstallPipelines, isTopLevelPipeline } from './install'; export { deletePreviousPipelines, deletePipeline } from './remove'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts index c6830d5bb9a03..da035a44c9921 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -6,14 +6,12 @@ */ import type { TransportRequestOptions } from '@elastic/elasticsearch'; -import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import { ElasticsearchAssetType } from '../../../../types'; import type { EsAssetReference, RegistryDataStream, InstallablePackage } from '../../../../types'; import { getAsset, getPathParts } from '../../archive'; import type { ArchiveEntry } from '../../archive'; -import { saveInstalledEsRefs } from '../../packages/install'; -import { getInstallationObject } from '../../packages'; import { FLEET_FINAL_PIPELINE_CONTENT, FLEET_FINAL_PIPELINE_ID, @@ -24,8 +22,6 @@ import { appendMetadataToIngestPipeline } from '../meta'; import { retryTransientEsErrors } from '../retry'; -import { deletePipelineRefs } from './remove'; - interface RewriteSubstitution { source: string; target: string; @@ -39,22 +35,23 @@ export const isTopLevelPipeline = (path: string) => { ); }; -export const installPipelines = async ( +export const prepareToInstallPipelines = ( installablePackage: InstallablePackage, - paths: string[], - esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract, - logger: Logger -) => { + paths: string[] +): { + assetsToAdd: EsAssetReference[]; + install: (esClient: ElasticsearchClient, logger: Logger) => Promise; +} => { // unlike other ES assets, pipeline names are versioned so after a template is updated // it can be created pointing to the new template, without removing the old one and effecting data // so do not remove the currently installed pipelines here const dataStreams = installablePackage.data_streams; - const { name: pkgName, version: pkgVersion } = installablePackage; + const { version: pkgVersion } = installablePackage; const pipelinePaths = paths.filter((path) => isPipeline(path)); const topLevelPipelinePaths = paths.filter((path) => isTopLevelPipeline(path)); - if (!dataStreams?.length && topLevelPipelinePaths.length === 0) return []; + if (!dataStreams?.length && topLevelPipelinePaths.length === 0) + return { assetsToAdd: [], install: () => Promise.resolve() }; // get and save pipeline refs before installing pipelines let pipelineRefs = dataStreams @@ -67,7 +64,7 @@ export const installPipelines = async ( const nameForInstallation = getPipelineNameForInstallation({ pipelineName: name, dataStream, - packageVersion: installablePackage.version, + packageVersion: pkgVersion, }); return { id: nameForInstallation, type: ElasticsearchAssetType.ingestPipeline }; }); @@ -80,57 +77,48 @@ export const installPipelines = async ( const { name } = getNameAndExtension(path); const nameForInstallation = getPipelineNameForInstallation({ pipelineName: name, - packageVersion: installablePackage.version, + packageVersion: pkgVersion, }); return { id: nameForInstallation, type: ElasticsearchAssetType.ingestPipeline }; }); pipelineRefs = [...pipelineRefs, ...topLevelPipelineRefs]; - // check that we don't duplicate the pipeline refs if the user is reinstalling - const installedPkg = await getInstallationObject({ - savedObjectsClient, - pkgName, - }); - if (!installedPkg) throw new Error("integration wasn't found while installing pipelines"); - // remove the current pipeline refs, if any exist, associated with this version before saving new ones so no duplicates occur - await deletePipelineRefs( - savedObjectsClient, - installedPkg.attributes.installed_es, - pkgName, - pkgVersion - ); - await saveInstalledEsRefs(savedObjectsClient, installablePackage.name, pipelineRefs); - const pipelines = dataStreams - ? dataStreams.reduce>>((acc, dataStream) => { - if (dataStream.ingest_pipeline) { - acc.push( - installAllPipelines({ - dataStream, - esClient, - logger, - paths: pipelinePaths, - installablePackage, - }) - ); - } - return acc; - }, []) - : []; - - if (topLevelPipelinePaths) { - pipelines.push( - installAllPipelines({ - dataStream: undefined, - esClient, - logger, - paths: topLevelPipelinePaths, - installablePackage, - }) - ); - } + return { + assetsToAdd: pipelineRefs, + install: async (esClient, logger) => { + const pipelines = dataStreams + ? dataStreams.reduce>>((acc, dataStream) => { + if (dataStream.ingest_pipeline) { + acc.push( + installAllPipelines({ + dataStream, + esClient, + logger, + paths: pipelinePaths, + installablePackage, + }) + ); + } + return acc; + }, []) + : []; + + if (topLevelPipelinePaths) { + pipelines.push( + installAllPipelines({ + dataStream: undefined, + esClient, + logger, + paths: topLevelPipelinePaths, + installablePackage, + }) + ); + } - return await Promise.all(pipelines).then((results) => results.flat()); + await Promise.all(pipelines); + }, + }; }; export function rewriteIngestPipeline( diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts index e9d693bdbfaa8..7e2b6c121bbab 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/remove.ts @@ -10,54 +10,38 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/ import { appContextService } from '../../..'; import { ElasticsearchAssetType } from '../../../../types'; import { IngestManagerError } from '../../../../errors'; -import { getInstallation } from '../../packages/get'; -import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; import type { EsAssetReference } from '../../../../../common'; +import { updateEsAssetReferences } from '../../packages/install'; export const deletePreviousPipelines = async ( esClient: ElasticsearchClient, savedObjectsClient: SavedObjectsClientContract, pkgName: string, - previousPkgVersion: string + previousPkgVersion: string, + esReferences: EsAssetReference[] ) => { const logger = appContextService.getLogger(); - const installation = await getInstallation({ savedObjectsClient, pkgName }); - if (!installation) return; - const installedEsAssets = installation.installed_es; - const installedPipelines = installedEsAssets.filter( + const installedPipelines = esReferences.filter( ({ type, id }) => type === ElasticsearchAssetType.ingestPipeline && id.includes(previousPkgVersion) ); - const deletePipelinePromises = installedPipelines.map(({ type, id }) => { - return deletePipeline(esClient, id); - }); - try { - await Promise.all(deletePipelinePromises); - } catch (e) { - logger.error(e); - } try { - await deletePipelineRefs(savedObjectsClient, installedEsAssets, pkgName, previousPkgVersion); + await Promise.all( + installedPipelines.map(({ type, id }) => { + return deletePipeline(esClient, id); + }) + ); } catch (e) { logger.error(e); } -}; -export const deletePipelineRefs = async ( - savedObjectsClient: SavedObjectsClientContract, - installedEsAssets: EsAssetReference[], - pkgName: string, - pkgVersion: string -) => { - const filteredAssets = installedEsAssets.filter(({ type, id }) => { - if (type !== ElasticsearchAssetType.ingestPipeline) return true; - if (!id.includes(pkgVersion)) return true; - return false; - }); - return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_es: filteredAssets, + return await updateEsAssetReferences(savedObjectsClient, pkgName, esReferences, { + assetsToRemove: esReferences.filter(({ type, id }) => { + return type === ElasticsearchAssetType.ingestPipeline && id.includes(previousPkgVersion); + }), }); }; + export async function deletePipeline(esClient: ElasticsearchClient, id: string): Promise { // '*' shouldn't ever appear here, but it still would delete all ingest pipelines if (id && id !== '*') { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts index 13b3de989e620..630433e18ce39 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ml_model/install.ts @@ -8,13 +8,14 @@ import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; import { errors } from '@elastic/elasticsearch'; -import { saveInstalledEsRefs } from '../../packages/install'; import { getPathParts } from '../../archive'; import { ElasticsearchAssetType } from '../../../../../common/types/models'; import type { EsAssetReference, InstallablePackage } from '../../../../../common/types/models'; import { retryTransientEsErrors } from '../retry'; +import { updateEsAssetReferences } from '../../packages/install'; + import { getAsset } from './common'; interface MlModelInstallation { @@ -27,11 +28,11 @@ export const installMlModel = async ( paths: string[], esClient: ElasticsearchClient, savedObjectsClient: SavedObjectsClientContract, - logger: Logger + logger: Logger, + esReferences: EsAssetReference[] ) => { const mlModelPath = paths.find((path) => isMlModel(path)); - const installedMlModels: EsAssetReference[] = []; if (mlModelPath !== undefined) { const content = getAsset(mlModelPath).toString('utf-8'); const pathParts = mlModelPath.split('/'); @@ -43,17 +44,22 @@ export const installMlModel = async ( }; // get and save ml model refs before installing ml model - await saveInstalledEsRefs(savedObjectsClient, installablePackage.name, [mlModelRef]); + esReferences = await updateEsAssetReferences( + savedObjectsClient, + installablePackage.name, + esReferences, + { assetsToAdd: [mlModelRef] } + ); const mlModel: MlModelInstallation = { installationName: modelId, content, }; - const result = await handleMlModelInstall({ esClient, logger, mlModel }); - installedMlModels.push(result); + await handleMlModelInstall({ esClient, logger, mlModel }); } - return installedMlModels; + + return esReferences; }; const isMlModel = (path: string) => { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts index 48f070434530a..3478da69bf721 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts @@ -4,30 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { loggerMock } from '@kbn/logging-mocks'; - import { createAppContextStartContractMock } from '../../../../mocks'; import { appContextService } from '../../..'; import type { RegistryDataStream } from '../../../../types'; -import type { Field } from '../../fields/field'; -import { installTemplate } from './install'; +import { prepareTemplate } from './install'; -describe('EPM install', () => { +describe('EPM index template install', () => { beforeEach(async () => { appContextService.start(createAppContextStartContractMock()); }); - it('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix not set', async () => { - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - esClient.indices.getIndexTemplate.mockImplementation(() => - elasticsearchServiceMock.createSuccessTransportRequestPromise({ index_templates: [] }) - ); - - const fields: Field[] = []; + it('tests prepareTemplate to use correct priority and index_patterns for data stream with dataset_is_prefix not set', async () => { const dataStreamDatasetIsPrefixUnset = { type: 'metrics', dataset: 'package.dataset', @@ -43,29 +32,14 @@ describe('EPM install', () => { }; const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; const templatePriorityDatasetIsPrefixUnset = 200; - await installTemplate({ - esClient, - logger: loggerMock.create(), - fields, - dataStream: dataStreamDatasetIsPrefixUnset, - packageVersion: pkg.version, - packageName: pkg.name, - }); - - const sentTemplate = ( - esClient.indices.putIndexTemplate.mock.calls[0][0] as estypes.IndicesPutIndexTemplateRequest - ).body; - - expect(sentTemplate).toBeDefined(); - expect(sentTemplate?.priority).toBe(templatePriorityDatasetIsPrefixUnset); - expect(sentTemplate?.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); + const { + indexTemplate: { indexTemplate }, + } = prepareTemplate({ pkg, dataStream: dataStreamDatasetIsPrefixUnset }); + expect(indexTemplate.priority).toBe(templatePriorityDatasetIsPrefixUnset); + expect(indexTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); }); - it('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to false', async () => { - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - esClient.indices.getIndexTemplate.mockResponse({ index_templates: [] }); - - const fields: Field[] = []; + it('tests prepareTemplate to use correct priority and index_patterns for data stream with dataset_is_prefix set to false', async () => { const dataStreamDatasetIsPrefixFalse = { type: 'metrics', dataset: 'package.dataset', @@ -82,29 +56,15 @@ describe('EPM install', () => { }; const templateIndexPatternDatasetIsPrefixFalse = 'metrics-package.dataset-*'; const templatePriorityDatasetIsPrefixFalse = 200; - await installTemplate({ - esClient, - logger: loggerMock.create(), - fields, - dataStream: dataStreamDatasetIsPrefixFalse, - packageVersion: pkg.version, - packageName: pkg.name, - }); - - const sentTemplate = ( - esClient.indices.putIndexTemplate.mock.calls[0][0] as estypes.IndicesPutIndexTemplateRequest - ).body; + const { + indexTemplate: { indexTemplate }, + } = prepareTemplate({ pkg, dataStream: dataStreamDatasetIsPrefixFalse }); - expect(sentTemplate).toBeDefined(); - expect(sentTemplate?.priority).toBe(templatePriorityDatasetIsPrefixFalse); - expect(sentTemplate?.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixFalse]); + expect(indexTemplate.priority).toBe(templatePriorityDatasetIsPrefixFalse); + expect(indexTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixFalse]); }); - it('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to true', async () => { - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - esClient.indices.getIndexTemplate.mockResponse({ index_templates: [] }); - - const fields: Field[] = []; + it('tests prepareTemplate to use correct priority and index_patterns for data stream with dataset_is_prefix set to true', async () => { const dataStreamDatasetIsPrefixTrue = { type: 'metrics', dataset: 'package.dataset', @@ -121,75 +81,11 @@ describe('EPM install', () => { }; const templateIndexPatternDatasetIsPrefixTrue = 'metrics-package.dataset.*-*'; const templatePriorityDatasetIsPrefixTrue = 150; - await installTemplate({ - esClient, - logger: loggerMock.create(), - fields, - dataStream: dataStreamDatasetIsPrefixTrue, - packageVersion: pkg.version, - packageName: pkg.name, - }); - - const sentTemplate = ( - esClient.indices.putIndexTemplate.mock.calls[0][0] as estypes.IndicesPutIndexTemplateRequest - ).body; - - expect(sentTemplate).toBeDefined(); - expect(sentTemplate?.priority).toBe(templatePriorityDatasetIsPrefixTrue); - expect(sentTemplate?.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixTrue]); - }); - - it('tests installPackage remove the aliases property if the property existed', async () => { - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - - esClient.indices.getIndexTemplate.mockResponse({ - index_templates: [ - { - name: 'metrics-package.dataset', - // @ts-expect-error not full interface - index_template: { - index_patterns: ['metrics-package.dataset-*'], - template: { aliases: {} }, - }, - }, - ], - }); - - const fields: Field[] = []; - const dataStreamDatasetIsPrefixUnset = { - type: 'metrics', - dataset: 'package.dataset', - title: 'test data stream', - release: 'experimental', - package: 'package', - path: 'path', - ingest_pipeline: 'default', - } as RegistryDataStream; - const pkg = { - name: 'package', - version: '0.0.1', - }; - const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*'; - const templatePriorityDatasetIsPrefixUnset = 200; - await installTemplate({ - esClient, - logger: loggerMock.create(), - fields, - dataStream: dataStreamDatasetIsPrefixUnset, - packageVersion: pkg.version, - packageName: pkg.name, - }); - - const removeAliases = ( - esClient.indices.putIndexTemplate.mock.calls[0][0] as estypes.IndicesPutIndexTemplateRequest - ).body; - expect(removeAliases?.template?.aliases).not.toBeDefined(); + const { + indexTemplate: { indexTemplate }, + } = prepareTemplate({ pkg, dataStream: dataStreamDatasetIsPrefixTrue }); - const sentTemplate = ( - esClient.indices.putIndexTemplate.mock.calls[1][0] as estypes.IndicesPutIndexTemplateRequest - ).body; - expect(sentTemplate).toBeDefined(); - expect(sentTemplate?.priority).toBe(templatePriorityDatasetIsPrefixUnset); - expect(sentTemplate?.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]); + expect(indexTemplate.priority).toBe(templatePriorityDatasetIsPrefixTrue); + expect(indexTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixTrue]); }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index 6d953835dfe6c..df6d9d84a08c5 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -7,7 +7,7 @@ import { merge } from 'lodash'; import Boom from '@hapi/boom'; -import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import { ElasticsearchAssetType } from '../../../../types'; import type { @@ -16,17 +16,16 @@ import type { RegistryElasticsearch, InstallablePackage, IndexTemplate, - PackageInfo, IndexTemplateMappings, TemplateMapEntry, TemplateMap, + EsAssetReference, + PackageInfo, } from '../../../../types'; import { loadFieldsFromYaml, processFields } from '../../fields/field'; -import type { Field } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; import { getAsset, getPathParts } from '../../archive'; -import { removeAssetTypesFromInstalledEs, saveInstalledEsRefs } from '../../packages/install'; import { FLEET_COMPONENT_TEMPLATES, PACKAGE_TEMPLATE_SUFFIX, @@ -36,8 +35,6 @@ import { import { getESAssetMetadata } from '../meta'; import { retryTransientEsErrors } from '../retry'; -import { getPackageInfo } from '../../packages'; - import { generateMappings, generateTemplateName, @@ -49,57 +46,55 @@ import { buildDefaultSettings } from './default_settings'; const FLEET_COMPONENT_TEMPLATE_NAMES = FLEET_COMPONENT_TEMPLATES.map((tmpl) => tmpl.name); -export const installTemplates = async ( +export const prepareToInstallTemplates = ( installablePackage: InstallablePackage, - esClient: ElasticsearchClient, - logger: Logger, paths: string[], - savedObjectsClient: SavedObjectsClientContract -): Promise => { - // install any pre-built index template assets, - // atm, this is only the base package's global index templates - // Install component templates first, as they are used by the index templates - await installPreBuiltComponentTemplates(paths, esClient, logger); - await installPreBuiltTemplates(paths, esClient, logger); - + esReferences: EsAssetReference[] +): { + assetsToAdd: EsAssetReference[]; + assetsToRemove: EsAssetReference[]; + install: (esClient: ElasticsearchClient, logger: Logger) => Promise; +} => { // remove package installation's references to index templates - await removeAssetTypesFromInstalledEs(savedObjectsClient, installablePackage.name, [ - ElasticsearchAssetType.indexTemplate, - ElasticsearchAssetType.componentTemplate, - ]); + const assetsToRemove = esReferences.filter( + ({ type }) => + type === ElasticsearchAssetType.indexTemplate || + type === ElasticsearchAssetType.componentTemplate + ); + // build templates per data stream from yml files const dataStreams = installablePackage.data_streams; - if (!dataStreams) return []; - - const packageInfo = await getPackageInfo({ - savedObjectsClient, - pkgName: installablePackage.name, - pkgVersion: installablePackage.version, - }); + if (!dataStreams) return { assetsToAdd: [], assetsToRemove, install: () => Promise.resolve([]) }; - const installedTemplatesNested = await Promise.all( - dataStreams.map((dataStream) => - installTemplateForDataStream({ - pkg: packageInfo, - esClient, - logger, - dataStream, - }) - ) + const templates = dataStreams.map((dataStream) => + prepareTemplate({ pkg: installablePackage, dataStream }) ); - const installedTemplates = installedTemplatesNested.flat(); - - // get template refs to save - const installedIndexTemplateRefs = getAllTemplateRefs(installedTemplates); + const assetsToAdd = getAllTemplateRefs(templates.map((template) => template.indexTemplate)); - // add package installation's references to index templates - await saveInstalledEsRefs( - savedObjectsClient, - installablePackage.name, - installedIndexTemplateRefs - ); + return { + assetsToAdd, + assetsToRemove, + install: async (esClient, logger) => { + // install any pre-built index template assets, + // atm, this is only the base package's global index templates + // Install component templates first, as they are used by the index templates + await installPreBuiltComponentTemplates(paths, esClient, logger); + await installPreBuiltTemplates(paths, esClient, logger); + + await Promise.all( + templates.map((template) => + installComponentAndIndexTemplateForDataStream({ + esClient, + logger, + componentTemplates: template.componentTemplates, + indexTemplate: template.indexTemplate, + }) + ) + ); - return installedTemplates; + return templates.map((template) => template.indexTemplate); + }, + }; }; const installPreBuiltTemplates = async ( @@ -181,31 +176,24 @@ const isComponentTemplate = (path: string) => { }; /** - * installTemplateForDataStream installs one template for each data stream + * installComponentAndIndexTemplateForDataStream installs one template for each data stream * * The template is currently loaded with the pkgkey-package-data_stream */ -export async function installTemplateForDataStream({ - pkg, +export async function installComponentAndIndexTemplateForDataStream({ esClient, logger, - dataStream, + componentTemplates, + indexTemplate, }: { - pkg: PackageInfo; esClient: ElasticsearchClient; logger: Logger; - dataStream: RegistryDataStream; -}): Promise { - const fields = await loadFieldsFromYaml(pkg, dataStream.path); - return installTemplate({ - esClient, - logger, - fields, - dataStream, - packageVersion: pkg.version, - packageName: pkg.name, - }); + componentTemplates: TemplateMap; + indexTemplate: IndexTemplateEntry; +}) { + await installDataStreamComponentTemplates({ esClient, logger, componentTemplates }); + await installTemplate({ esClient, logger, template: indexTemplate }); } function putComponentTemplate( @@ -285,49 +273,33 @@ function buildComponentTemplates(params: { return templatesMap; } -async function installDataStreamComponentTemplates(params: { - mappings: IndexTemplateMappings; - templateName: string; - registryElasticsearch: RegistryElasticsearch | undefined; +async function installDataStreamComponentTemplates({ + esClient, + logger, + componentTemplates, +}: { esClient: ElasticsearchClient; logger: Logger; - packageName: string; - defaultSettings: IndexTemplate['template']['settings']; + componentTemplates: TemplateMap; }) { - const { - templateName, - registryElasticsearch, - esClient, - packageName, - defaultSettings, - logger, - mappings, - } = params; - const componentTemplates = buildComponentTemplates({ - mappings, - templateName, - registryElasticsearch, - packageName, - defaultSettings, - }); - const templateEntries = Object.entries(componentTemplates); // TODO: Check return values for errors await Promise.all( - templateEntries.map(async ([name, body]) => { + Object.entries(componentTemplates).map(async ([name, body]) => { if (isUserSettingsTemplate(name)) { - // look for existing user_settings template - const result = await retryTransientEsErrors( - () => esClient.cluster.getComponentTemplate({ name }, { ignore: [404] }), - { logger } - ); - const hasUserSettingsTemplate = result.component_templates?.length === 1; - if (!hasUserSettingsTemplate) { - // only add if one isn't already present + try { + // Attempt to create custom component templates, ignore if they already exist const { clusterPromise } = putComponentTemplate(esClient, logger, { body, name, + create: true, }); - return clusterPromise; + return await clusterPromise; + } catch (e) { + if (e?.statusCode === 400 && e.body?.error?.reason.includes('already exists')) { + // ignore + } else { + throw e; + } } } else { const { clusterPromise } = putComponentTemplate(esClient, logger, { body, name }); @@ -335,8 +307,6 @@ async function installDataStreamComponentTemplates(params: { } }) ); - - return { componentTemplateNames: Object.keys(componentTemplates) }; } export async function ensureDefaultComponentTemplates( @@ -380,21 +350,15 @@ export async function ensureComponentTemplate( return { isCreated: !existingTemplate }; } -export async function installTemplate({ - esClient, - logger, - fields, +export function prepareTemplate({ + pkg, dataStream, - packageVersion, - packageName, }: { - esClient: ElasticsearchClient; - logger: Logger; - fields: Field[]; + pkg: Pick; dataStream: RegistryDataStream; - packageVersion: string; - packageName: string; -}): Promise { +}): { componentTemplates: TemplateMap; indexTemplate: IndexTemplateEntry } { + const { name: packageName, version: packageVersion } = pkg; + const fields = loadFieldsFromYaml(pkg, dataStream.path); const validFields = processFields(fields); const mappings = generateMappings(validFields); const templateName = generateTemplateName(dataStream); @@ -410,44 +374,6 @@ export async function installTemplate({ }); } - // Datastream now throw an error if the aliases field is present so ensure that we remove that field. - const getTemplateRes = await retryTransientEsErrors( - () => - esClient.indices.getIndexTemplate( - { - name: templateName, - }, - { - ignore: [404], - } - ), - { logger } - ); - - const existingIndexTemplate = getTemplateRes?.index_templates?.[0]; - if ( - existingIndexTemplate && - existingIndexTemplate.name === templateName && - existingIndexTemplate?.index_template?.template?.aliases - ) { - const updateIndexTemplateParams = { - name: templateName, - body: { - ...existingIndexTemplate.index_template, - template: { - ...existingIndexTemplate.index_template.template, - // Remove the aliases field - aliases: undefined, - }, - }, - }; - - await retryTransientEsErrors( - () => esClient.indices.putIndexTemplate(updateIndexTemplateParams, { ignore: [404] }), - { logger } - ); - } - const defaultSettings = buildDefaultSettings({ templateName, packageName, @@ -456,40 +382,51 @@ export async function installTemplate({ ilmPolicy: dataStream.ilm_policy, }); - const { componentTemplateNames } = await installDataStreamComponentTemplates({ + const componentTemplates = buildComponentTemplates({ + defaultSettings, mappings, + packageName, templateName, registryElasticsearch: dataStream.elasticsearch, - esClient, - logger, - packageName, - defaultSettings, }); const template = getTemplate({ templateIndexPattern, pipelineName, packageName, - composedOfTemplates: componentTemplateNames, + composedOfTemplates: Object.keys(componentTemplates), templatePriority, hidden: dataStream.hidden, }); + return { + componentTemplates, + indexTemplate: { + templateName, + indexTemplate: template, + }, + }; +} + +async function installTemplate({ + esClient, + logger, + template, +}: { + esClient: ElasticsearchClient; + logger: Logger; + template: IndexTemplateEntry; +}) { // TODO: Check return values for errors const esClientParams = { - name: templateName, - body: template, + name: template.templateName, + body: template.indexTemplate, }; await retryTransientEsErrors( () => esClient.indices.putIndexTemplate(esClientParams, { ignore: [404] }), { logger } ); - - return { - templateName, - indexTemplate: template, - }; } export function getAllTemplateRefs(installedTemplates: IndexTemplateEntry[]) { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts index fea12f4b139c6..ab8f60e172dcb 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts @@ -8,7 +8,7 @@ import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server'; import { errors } from '@elastic/elasticsearch'; -import { saveInstalledEsRefs } from '../../packages/install'; +import { updateEsAssetReferences } from '../../packages/install'; import { getPathParts } from '../../archive'; import { ElasticsearchAssetType } from '../../../../../common/types/models'; import type { EsAssetReference, InstallablePackage } from '../../../../../common/types/models'; @@ -18,7 +18,7 @@ import { getESAssetMetadata } from '../meta'; import { retryTransientEsErrors } from '../retry'; -import { deleteTransforms, deleteTransformRefs } from './remove'; +import { deleteTransforms } from './remove'; import { getAsset } from './common'; interface TransformInstallation { @@ -31,12 +31,14 @@ export const installTransform = async ( paths: string[], esClient: ElasticsearchClient, savedObjectsClient: SavedObjectsClientContract, - logger: Logger + logger: Logger, + esReferences?: EsAssetReference[] ) => { const installation = await getInstallation({ savedObjectsClient, pkgName: installablePackage.name, }); + esReferences = esReferences ?? installation?.installed_es ?? []; let previousInstalledTransformEsAssets: EsAssetReference[] = []; if (installation) { previousInstalledTransformEsAssets = installation.installed_es.filter( @@ -71,7 +73,14 @@ export const installTransform = async ( }, []); // get and save transform refs before installing transforms - await saveInstalledEsRefs(savedObjectsClient, installablePackage.name, transformRefs); + esReferences = await updateEsAssetReferences( + savedObjectsClient, + installablePackage.name, + esReferences, + { + assetsToAdd: transformRefs, + } + ); const transforms: TransformInstallation[] = transformPaths.map((path: string) => { const content = JSON.parse(getAsset(path).toString('utf-8')); @@ -95,21 +104,17 @@ export const installTransform = async ( } if (previousInstalledTransformEsAssets.length > 0) { - const currentInstallation = await getInstallation({ + esReferences = await updateEsAssetReferences( savedObjectsClient, - pkgName: installablePackage.name, - }); - - // remove the saved object reference - await deleteTransformRefs( - savedObjectsClient, - currentInstallation?.installed_es || [], installablePackage.name, - previousInstalledTransformEsAssets.map((asset) => asset.id), - installedTransforms.map((installed) => installed.id) + esReferences, + { + assetsToRemove: previousInstalledTransformEsAssets, + } ); } - return installedTransforms; + + return { installedTransforms, esReferences }; }; export const isTransform = (path: string) => { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts index 16384b8bfba19..74e49031861c1 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform.test.ts @@ -34,8 +34,10 @@ import { appContextService } from '../../../app_context'; import { getESAssetMetadata } from '../meta'; -import { installTransform } from './install'; +import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../constants'; + import { getAsset } from './common'; +import { installTransform } from './install'; describe('test transform install', () => { let esClient: ReturnType; @@ -46,6 +48,12 @@ describe('test transform install', () => { (getInstallation as jest.MockedFunction).mockReset(); (getInstallationObject as jest.MockedFunction).mockReset(); savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.update.mockImplementation(async (type, id, attributes) => ({ + type: PACKAGES_SAVED_OBJECT_TYPE, + id: 'endpoint', + attributes, + references: [], + })); }); afterEach(() => { @@ -158,7 +166,8 @@ describe('test transform install', () => { ], esClient, savedObjectsClient, - loggerMock.create() + loggerMock.create(), + previousInstallation.installed_es ); expect(esClient.transform.getTransform.mock.calls).toEqual([ @@ -255,6 +264,9 @@ describe('test transform install', () => { }, ], }, + { + refresh: false, + }, ], [ 'epm-packages', @@ -266,15 +278,18 @@ describe('test transform install', () => { type: 'ingest_pipeline', }, { - id: 'endpoint.metadata_current-default-0.16.0-dev.0', + id: 'endpoint.metadata-default-0.16.0-dev.0', type: 'transform', }, { - id: 'endpoint.metadata-default-0.16.0-dev.0', + id: 'endpoint.metadata_current-default-0.16.0-dev.0', type: 'transform', }, ], }, + { + refresh: false, + }, ], ]); }); @@ -331,7 +346,8 @@ describe('test transform install', () => { ['endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/default.json'], esClient, savedObjectsClient, - loggerMock.create() + loggerMock.create(), + previousInstallation.installed_es ); const meta = getESAssetMetadata({ packageName: 'endpoint' }); @@ -363,6 +379,9 @@ describe('test transform install', () => { { id: 'endpoint.metadata_current-default-0.16.0-dev.0', type: 'transform' }, ], }, + { + refresh: false, + }, ], ]); }); @@ -443,7 +462,8 @@ describe('test transform install', () => { [], esClient, savedObjectsClient, - loggerMock.create() + loggerMock.create(), + previousInstallation.installed_es ); expect(esClient.transform.getTransform.mock.calls).toEqual([ @@ -492,6 +512,9 @@ describe('test transform install', () => { { installed_es: [], }, + { + refresh: false, + }, ], ]); }); @@ -559,7 +582,8 @@ describe('test transform install', () => { ['endpoint-0.16.0-dev.0/elasticsearch/transform/metadata_current/default.json'], esClient, savedObjectsClient, - loggerMock.create() + loggerMock.create(), + previousInstallation.installed_es ); const meta = getESAssetMetadata({ packageName: 'endpoint' }); @@ -586,6 +610,9 @@ describe('test transform install', () => { { id: 'endpoint.metadata_current-default-0.16.0-dev.0', type: 'transform' }, ], }, + { + refresh: false, + }, ], ]); }); diff --git a/x-pack/plugins/fleet/server/services/epm/fields/field.ts b/x-pack/plugins/fleet/server/services/epm/fields/field.ts index f1ad96504594e..0e00840b0c74e 100644 --- a/x-pack/plugins/fleet/server/services/epm/fields/field.ts +++ b/x-pack/plugins/fleet/server/services/epm/fields/field.ts @@ -261,12 +261,12 @@ const isFields = (path: string) => { * Gets all field files, optionally filtered by dataset, extracts .yml files, merges them together */ -export const loadFieldsFromYaml = async ( - pkg: PackageInfo, +export const loadFieldsFromYaml = ( + pkg: Pick, datasetName?: string -): Promise => { +): Field[] => { // Fetch all field definition files - const fieldDefinitionFiles = await getAssetsData(pkg, isFields, datasetName); + const fieldDefinitionFiles = getAssetsData(pkg, isFields, datasetName); return fieldDefinitionFiles.reduce((acc, file) => { // Make sure it is defined as it is optional. Should never happen. if (file.buffer) { diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index ce8d7e7be2bb9..b9582ce1cf148 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -21,9 +21,11 @@ import { partition } from 'lodash'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; import { getAsset, getPathParts } from '../../archive'; import { KibanaAssetType, KibanaSavedObjectType } from '../../../../types'; -import type { AssetType, AssetReference, AssetParts } from '../../../../types'; +import type { AssetType, AssetReference, AssetParts, Installation } from '../../../../types'; import { savedObjectTypes } from '../../packages'; import { indexPatternTypes, getIndexPatternSavedObjects } from '../index_pattern/install'; +import { saveKibanaAssetsRefs } from '../../packages/install'; +import { deleteKibanaSavedObjectsAssets } from '../../packages/remove'; type SavedObjectsImporterContract = Pick; const formatImportErrorsForLog = (errors: SavedObjectsImportFailure[]) => @@ -121,6 +123,41 @@ export async function installKibanaAssets(options: { return installedAssets; } + +export async function installKibanaAssetsAndReferences({ + savedObjectsClient, + savedObjectsImporter, + logger, + pkgName, + paths, + installedPkg, +}: { + savedObjectsClient: SavedObjectsClientContract; + savedObjectsImporter: Pick; + logger: Logger; + pkgName: string; + paths: string[]; + installedPkg?: SavedObject; +}) { + const kibanaAssets = await getKibanaAssets(paths); + if (installedPkg) await deleteKibanaSavedObjectsAssets({ savedObjectsClient, installedPkg }); + // save new kibana refs before installing the assets + const installedKibanaAssetsRefs = await saveKibanaAssetsRefs( + savedObjectsClient, + pkgName, + kibanaAssets + ); + + await installKibanaAssets({ + logger, + savedObjectsImporter, + pkgName, + kibanaAssets, + }); + + return installedKibanaAssetsRefs; +} + export const deleteKibanaInstalledRefs = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, @@ -230,6 +267,7 @@ export async function installKibanaSavedObjects({ overwrite: true, readStream: createListStream(toBeSavedObjects), createNewCopies: false, + refresh: false, }) ); diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts index 31bf9e47a4ae0..782af2860d2e3 100644 --- a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts @@ -50,6 +50,7 @@ function getTest( spy: jest.SpyInstance; spyArgs: any[]; spyResponse: any; + expectedReturnValue: any; }; switch (testKey) { @@ -65,6 +66,7 @@ function getTest( }, ], spyResponse: { name: 'getInstallation test' }, + expectedReturnValue: { name: 'getInstallation test' }, }; break; case testKeys[1]: @@ -82,6 +84,7 @@ function getTest( }, ], spyResponse: { name: 'ensureInstalledPackage test' }, + expectedReturnValue: { name: 'ensureInstalledPackage test' }, }; break; case testKeys[2]: @@ -91,6 +94,7 @@ function getTest( spy: jest.spyOn(epmRegistry, 'fetchFindLatestPackageOrThrow'), spyArgs: ['package name'], spyResponse: { name: 'fetchFindLatestPackage test' }, + expectedReturnValue: { name: 'fetchFindLatestPackage test' }, }; break; case testKeys[3]: @@ -103,6 +107,10 @@ function getTest( packageInfo: { name: 'getRegistryPackage test' }, paths: ['/some/test/path'], }, + expectedReturnValue: { + packageInfo: { name: 'getRegistryPackage test' }, + paths: ['/some/test/path'], + }, }; break; case testKeys[4]: @@ -122,7 +130,14 @@ function getTest( args: [pkg, paths], spy: jest.spyOn(epmTransformsInstall, 'installTransform'), spyArgs: [pkg, paths, mocks.esClient, mocks.soClient, mocks.logger], - spyResponse: [ + spyResponse: { + installedTransforms: [ + { + name: 'package name', + }, + ], + }, + expectedReturnValue: [ { name: 'package name', }, @@ -176,10 +191,13 @@ describe('PackageService', () => { soClient: mockSoClient, logger: mockLogger, }; - const { method, args, spy, spyArgs, spyResponse } = getTest(mockClients, testKey); + const { method, args, spy, spyArgs, spyResponse, expectedReturnValue } = getTest( + mockClients, + testKey + ); spy.mockResolvedValue(spyResponse); - await expect(method(...args)).resolves.toEqual(spyResponse); + await expect(method(...args)).resolves.toEqual(expectedReturnValue); expect(spy).toHaveBeenCalledWith(...spyArgs); }); }); @@ -193,10 +211,13 @@ describe('PackageService', () => { soClient: mockSoClient, logger: mockLogger, }; - const { method, args, spy, spyArgs, spyResponse } = getTest(mockClients, testKey); + const { method, args, spy, spyArgs, spyResponse, expectedReturnValue } = getTest( + mockClients, + testKey + ); spy.mockResolvedValue(spyResponse); - await expect(method(...args)).resolves.toEqual(spyResponse); + await expect(method(...args)).resolves.toEqual(expectedReturnValue); expect(spy).toHaveBeenCalledWith(...spyArgs); }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.ts b/x-pack/plugins/fleet/server/services/epm/package_service.ts index 573ca3508e947..e16d4954f0b9d 100644 --- a/x-pack/plugins/fleet/server/services/epm/package_service.ts +++ b/x-pack/plugins/fleet/server/services/epm/package_service.ts @@ -146,14 +146,15 @@ class PackageClientImpl implements PackageClient { return installedAssets; } - #reinstallTransforms(packageInfo: InstallablePackage, paths: string[]) { - return installTransform( + async #reinstallTransforms(packageInfo: InstallablePackage, paths: string[]) { + const { installedTransforms } = await installTransform( packageInfo, paths, this.internalEsClient, this.internalSoClient, this.logger ); + return installedTransforms; } #runPreflight() { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts index c0e4404345902..db9803ea70f3a 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts @@ -21,16 +21,15 @@ jest.mock('./install'); jest.mock('./get'); import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; -import { installKibanaAssets } from '../kibana/assets/install'; +import { installKibanaAssetsAndReferences } from '../kibana/assets/install'; import { _installPackage } from './_install_package'; const mockedUpdateCurrentWriteIndices = updateCurrentWriteIndices as jest.MockedFunction< typeof updateCurrentWriteIndices >; -const mockedGetKibanaAssets = installKibanaAssets as jest.MockedFunction< - typeof installKibanaAssets ->; +const mockedInstallKibanaAssetsAndReferences = + installKibanaAssetsAndReferences as jest.MockedFunction; function sleep(millis: number) { return new Promise((resolve) => setTimeout(resolve, millis)); @@ -50,7 +49,7 @@ describe('_installPackage', () => { }); it('handles errors from installKibanaAssets', async () => { // force errors from this function - mockedGetKibanaAssets.mockImplementation(async () => { + mockedInstallKibanaAssetsAndReferences.mockImplementation(async () => { throw new Error('mocked async error A: should be caught'); }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 796269eee38b1..0124bff41736f 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -22,16 +22,15 @@ import { import type { InstallablePackage, InstallSource, PackageAssetReference } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import type { AssetReference, Installation, InstallType } from '../../../types'; -import { installTemplates } from '../elasticsearch/template/install'; +import { prepareToInstallTemplates } from '../elasticsearch/template/install'; import { removeLegacyTemplates } from '../elasticsearch/template/remove_legacy'; import { - installPipelines, + prepareToInstallPipelines, isTopLevelPipeline, deletePreviousPipelines, } from '../elasticsearch/ingest_pipeline'; -import { getAllTemplateRefs } from '../elasticsearch/template/install'; import { installILMPolicy } from '../elasticsearch/ilm/install'; -import { installKibanaAssets, getKibanaAssets } from '../kibana/assets/install'; +import { installKibanaAssetsAndReferences } from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; import { installTransform } from '../elasticsearch/transform/install'; import { installMlModel } from '../elasticsearch/ml_model'; @@ -40,8 +39,7 @@ import { saveArchiveEntries } from '../archive/storage'; import { ConcurrentInstallOperationError } from '../../../errors'; import { packagePolicyService } from '../..'; -import { createInstallation, saveKibanaAssetsRefs, updateVersion } from './install'; -import { deleteKibanaSavedObjectsAssets } from './remove'; +import { createInstallation, updateEsAssetReferences } from './install'; import { withPackageSpan } from './utils'; // this is only exported for testing @@ -106,48 +104,88 @@ export async function _installPackage({ }); } - const installedKibanaAssetsRefs = await withPackageSpan('Install Kibana assets', async () => { - const kibanaAssets = await getKibanaAssets(paths); - if (installedPkg) await deleteKibanaSavedObjectsAssets({ savedObjectsClient, installedPkg }); - // save new kibana refs before installing the assets - const assetRefs = await saveKibanaAssetsRefs(savedObjectsClient, pkgName, kibanaAssets); - - await installKibanaAssets({ - logger, + const kibanaAssetPromise = withPackageSpan('Install Kibana assets', () => + installKibanaAssetsAndReferences({ + savedObjectsClient, savedObjectsImporter, pkgName, - kibanaAssets, - }); + paths, + installedPkg, + logger, + }) + ); + // Necessary to avoid async promise rejection warning + // See https://stackoverflow.com/questions/40920179/should-i-refrain-from-handling-promise-rejection-asynchronously + kibanaAssetPromise.catch(() => {}); - return assetRefs; - }); + // Use a shared array that is updated by each operation. This allows each operation to accurately update the + // installation object with it's references without requiring a refresh of the SO index on each update (faster). + let esReferences = installedPkg?.attributes.installed_es ?? []; // the rest of the installation must happen in sequential order // currently only the base package has an ILM policy // at some point ILM policies can be installed/modified // per data stream and we should then save them - await withPackageSpan('Install ILM policies', () => - installILMPolicy(packageInfo, paths, esClient, logger) + esReferences = await withPackageSpan('Install ILM policies', () => + installILMPolicy(packageInfo, paths, esClient, savedObjectsClient, logger, esReferences) ); - const installedDataStreamIlm = await withPackageSpan('Install Data Stream ILM policies', () => - installIlmForDataStream(packageInfo, paths, esClient, savedObjectsClient, logger) - ); + ({ esReferences } = await withPackageSpan('Install Data Stream ILM policies', () => + installIlmForDataStream( + packageInfo, + paths, + esClient, + savedObjectsClient, + logger, + esReferences + ) + )); // installs ml models - const installedMlModel = await withPackageSpan('Install ML models', () => - installMlModel(packageInfo, paths, esClient, savedObjectsClient, logger) + esReferences = await withPackageSpan('Install ML models', () => + installMlModel(packageInfo, paths, esClient, savedObjectsClient, logger, esReferences) ); - // installs versionized pipelines without removing currently installed ones - const installedPipelines = await withPackageSpan('Install ingest pipelines', () => - installPipelines(packageInfo, paths, esClient, savedObjectsClient, logger) - ); - // install or update the templates referencing the newly installed pipelines - const installedTemplates = await withPackageSpan('Install index templates', () => - installTemplates(packageInfo, esClient, logger, paths, savedObjectsClient) + /** + * In order to install assets in parallel, we need to split the preparation step from the installation step. This + * allows us to know which asset references are going to be installed so that we can save them on the packages + * SO before installation begins. In the case of a failure during installing any individual asset, we'll have the + * references necessary to remove any assets in that were successfully installed during the rollback phase. + * + * This split of prepare/install could be extended to all asset types. Besides performance, it also allows us to + * more easily write unit tests against the asset generation code without needing to mock ES responses. + */ + const preparedIngestPipelines = prepareToInstallPipelines(packageInfo, paths); + const preparedIndexTemplates = prepareToInstallTemplates(packageInfo, paths, esReferences); + + // Update the references for the templates and ingest pipelines together. Need to be done togther to avoid race + // conditions on updating the installed_es field at the same time + // These must be saved before we actually attempt to install the templates or pipelines so that we know what to + // cleanup in the case that a single asset fails to install. + esReferences = await updateEsAssetReferences( + savedObjectsClient, + packageInfo.name, + esReferences, + { + assetsToRemove: preparedIndexTemplates.assetsToRemove, + assetsToAdd: [ + ...preparedIngestPipelines.assetsToAdd, + ...preparedIndexTemplates.assetsToAdd, + ], + } ); + // Install index templates and ingest pipelines in parallel since they typically take the longest + const [installedTemplates] = await Promise.all([ + withPackageSpan('Install index templates', () => + preparedIndexTemplates.install(esClient, logger) + ), + // installs versionized pipelines without removing currently installed ones + withPackageSpan('Install ingest pipelines', () => + preparedIngestPipelines.install(esClient, logger) + ), + ]); + try { await removeLegacyTemplates({ packageInfo, esClient, logger }); } catch (e) { @@ -159,9 +197,9 @@ export async function _installPackage({ updateCurrentWriteIndices(esClient, logger, installedTemplates) ); - const installedTransforms = await withPackageSpan('Install transforms', () => - installTransform(packageInfo, paths, esClient, savedObjectsClient, logger) - ); + ({ esReferences } = await withPackageSpan('Install transforms', () => + installTransform(packageInfo, paths, esClient, savedObjectsClient, logger, esReferences) + )); // If this is an update or retrying an update, delete the previous version's pipelines // Top-level pipeline assets will not be removed on upgrade as of ml model package addition which requires previous @@ -171,28 +209,30 @@ export async function _installPackage({ (installType === 'update' || installType === 'reupdate') && installedPkg ) { - await withPackageSpan('Delete previous ingest pipelines', () => + esReferences = await withPackageSpan('Delete previous ingest pipelines', () => deletePreviousPipelines( esClient, savedObjectsClient, pkgName, - installedPkg.attributes.version + installedPkg!.attributes.version, + esReferences ) ); } // pipelines from a different version may have installed during a failed update if (installType === 'rollback' && installedPkg) { - await await withPackageSpan('Delete previous ingest pipelines', () => + esReferences = await withPackageSpan('Delete previous ingest pipelines', () => deletePreviousPipelines( esClient, savedObjectsClient, pkgName, - installedPkg.attributes.install_version + installedPkg!.attributes.install_version, + esReferences ) ); } - const installedTemplateRefs = getAllTemplateRefs(installedTemplates); + const installedKibanaAssetsRefs = await kibanaAssetPromise; const packageAssetResults = await withPackageSpan('Update archive entries', () => saveArchiveEntries({ savedObjectsClient, @@ -208,11 +248,9 @@ export async function _installPackage({ }) ); - // update to newly installed version when all assets are successfully installed - if (installedPkg) await updateVersion(savedObjectsClient, pkgName, pkgVersion); - const updatedPackage = await withPackageSpan('Update install status', () => savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + version: pkgVersion, install_version: pkgVersion, install_status: 'installed', package_assets: packageAssetRefs, @@ -233,14 +271,7 @@ export async function _installPackage({ }); } - return [ - ...installedKibanaAssetsRefs, - ...installedPipelines, - ...installedDataStreamIlm, - ...installedTemplateRefs, - ...installedTransforms, - ...installedMlModel, - ]; + return [...installedKibanaAssetsRefs, ...esReferences]; } catch (err) { if (savedObjectsClient.errors.isConflictError(err)) { throw new ConcurrentInstallOperationError( diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts index c939ce093a65c..d67e76f90e551 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts @@ -17,7 +17,7 @@ import type { ArchiveEntry } from '../archive'; // and different package and version structure export function getAssets( - packageInfo: PackageInfo, + packageInfo: Pick, filter = (path: string): boolean => true, datasetName?: string ): string[] { @@ -51,11 +51,11 @@ export function getAssets( // ASK: Does getAssetsData need an installSource now? // if so, should it be an Installation vs InstallablePackage or add another argument? -export async function getAssetsData( - packageInfo: PackageInfo, +export function getAssetsData( + packageInfo: Pick, filter = (path: string): boolean => true, datasetName?: string -): Promise { +): ArchiveEntry[] { // Gather all asset data const assets = getAssets(packageInfo, filter, datasetName); const entries: ArchiveEntry[] = assets.map((path) => { 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 9ae549982399c..c7fc01c89eb06 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -17,6 +17,8 @@ import type { import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; +import pRetry from 'p-retry'; + import { generateESIndexPatterns } from '../elasticsearch/template/template'; import type { BulkInstallPackageInfo, @@ -29,13 +31,7 @@ import { IngestManagerError, PackageOutdatedError } from '../../../errors'; import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants'; import type { KibanaAssetType } from '../../../types'; import { licenseService } from '../..'; -import type { - Installation, - AssetType, - EsAssetReference, - InstallType, - InstallResult, -} from '../../../types'; +import type { Installation, EsAssetReference, InstallType, InstallResult } from '../../../types'; import { appContextService } from '../../app_context'; import * as Registry from '../registry'; import { @@ -271,10 +267,13 @@ async function installPackageFromRegistry({ installType, }); - // get latest package version - const latestPackage = await Registry.fetchFindLatestPackageOrThrow(pkgName, { - ignoreConstraints, - }); + // get latest package version and requested version in parallel for performance + const [latestPackage, { paths, packageInfo }] = await Promise.all([ + Registry.fetchFindLatestPackageOrThrow(pkgName, { + ignoreConstraints, + }), + Registry.getRegistryPackage(pkgName, pkgVersion), + ]); // let the user install if using the force flag or needing to reinstall or install a previous version due to failed update const installOutOfDateVersionOk = @@ -319,9 +318,6 @@ async function installPackageFromRegistry({ ); } - // get package info - const { paths, packageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion); - if (!licenseService.hasAtLeast(packageInfo.license || 'basic')) { const err = new Error(`Requires ${packageInfo.license} license`); sendEvent({ @@ -632,22 +628,60 @@ export const saveKibanaAssetsRefs = async ( kibanaAssets: Record ) => { const assetRefs = Object.values(kibanaAssets).flat().map(toAssetReference); - await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_kibana: assetRefs, - }); + // Because Kibana assets are installed in parallel with ES assets with refresh: false, we almost always run into an + // issue that causes a conflict error due to this issue: https://github.com/elastic/kibana/issues/126240. This is safe + // to retry constantly until it succeeds to optimize this critical user journey path as much as possible. + pRetry( + () => + savedObjectsClient.update( + PACKAGES_SAVED_OBJECT_TYPE, + pkgName, + { + installed_kibana: assetRefs, + }, + { refresh: false } + ), + { retries: 20 } // Use a number of retries higher than the number of es asset update operations + ); + return assetRefs; }; -export const saveInstalledEsRefs = async ( +/** + * Utility function for updating the installed_es field of a package + */ +export const updateEsAssetReferences = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, - installedAssets: EsAssetReference[] -) => { - const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const installedAssetsToSave = installedPkg?.attributes.installed_es.concat(installedAssets); + currentAssets: EsAssetReference[], + { + assetsToAdd = [], + assetsToRemove = [], + refresh = false, + }: { + assetsToAdd?: EsAssetReference[]; + assetsToRemove?: EsAssetReference[]; + /** + * Whether or not the update should force a refresh on the SO index. + * Defaults to `false` for faster updates, should only be `wait_for` if the update needs to be queried back from ES + * immediately. + */ + refresh?: 'wait_for' | false; + } +): Promise => { + const withAssetsRemoved = currentAssets.filter(({ type, id }) => { + if ( + assetsToRemove.some( + ({ type: removeType, id: removeId }) => removeType === type && removeId === id + ) + ) { + return false; + } + return true; + }); const deduplicatedAssets = - installedAssetsToSave?.reduce((acc, currentAsset) => { + [...withAssetsRemoved, ...assetsToAdd].reduce((acc, currentAsset) => { const foundAsset = acc.find((asset: EsAssetReference) => asset.id === currentAsset.id); if (!foundAsset) { return acc.concat([currentAsset]); @@ -656,27 +690,30 @@ export const saveInstalledEsRefs = async ( } }, [] as EsAssetReference[]) || []; - await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_es: deduplicatedAssets, - }); - return installedAssets; -}; - -export const removeAssetTypesFromInstalledEs = async ( - savedObjectsClient: SavedObjectsClientContract, - pkgName: string, - assetTypes: AssetType[] -) => { - const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const installedAssets = installedPkg?.attributes.installed_es; - if (!installedAssets?.length) return; - const installedAssetsToSave = installedAssets?.filter( - (asset) => !assetTypes.includes(asset.type) - ); + const { + attributes: { installed_es: updatedAssets }, + } = + // Because Kibana assets are installed in parallel with ES assets with refresh: false, we almost always run into an + // issue that causes a conflict error due to this issue: https://github.com/elastic/kibana/issues/126240. This is safe + // to retry constantly until it succeeds to optimize this critical user journey path as much as possible. + await pRetry( + () => + savedObjectsClient.update( + PACKAGES_SAVED_OBJECT_TYPE, + pkgName, + { + installed_es: deduplicatedAssets, + }, + { + refresh, + } + ), + // Use a lower number of retries for ES assets since they're installed in serial and can only conflict with + // the single Kibana update call. + { retries: 5 } + ); - return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_es: installedAssetsToSave, - }); + return updatedAssets ?? []; }; export async function ensurePackagesCompletedInstall( diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 7edf5b6020be8..95e65acfebef6 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -130,6 +130,8 @@ function deleteESAssets( return deleteTransforms(esClient, [id]); } else if (assetType === ElasticsearchAssetType.dataStreamIlmPolicy) { return deleteIlms(esClient, [id]); + } else if (assetType === ElasticsearchAssetType.ilmPolicy) { + return deleteIlms(esClient, [id]); } else if (assetType === ElasticsearchAssetType.mlModel) { return deleteMlModel(esClient, [id]); } diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index 2ae531f63379d..1074e975d3f6f 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -152,7 +152,8 @@ export async function fetchFindLatestPackageOrUndefined( export async function fetchInfo(pkgName: string, pkgVersion: string): Promise { const registryUrl = getRegistryUrl(); try { - const res = await fetchUrl(`${registryUrl}/package/${pkgName}/${pkgVersion}`).then(JSON.parse); + // Trailing slash avoids 301 redirect / extra hop + const res = await fetchUrl(`${registryUrl}/package/${pkgName}/${pkgVersion}/`).then(JSON.parse); return res; } catch (err) { diff --git a/x-pack/plugins/graph/README.md b/x-pack/plugins/graph/README.md index a0d7eb25ff987..b17f114a8f01f 100644 --- a/x-pack/plugins/graph/README.md +++ b/x-pack/plugins/graph/README.md @@ -10,8 +10,8 @@ Graph shows only up in the side bar if your server is running on a platinum or t * Run type check `node scripts/type_check.js --project=x-pack/tsconfig.json` * Run linter `node scripts/eslint.js x-pack/plugins/graph` * Run functional tests (make sure to stop dev server) - * Server `cd x-pack && node ./scripts/functional_tests_server.js` - * Tests `cd x-pack && node ../scripts/functional_test_runner.js --config ./test/functional/config.js --grep=graph` + * Server `node ./scripts/functional_tests_server.js --config x-pack/test/functional/apps/graph/config.ts` + * Tests `node scripts/functional_test_runner.js --config x-pack/test/functional/apps/graph/config.ts` ## Folder structure diff --git a/x-pack/plugins/graph/public/components/_search_bar.scss b/x-pack/plugins/graph/public/components/_search_bar.scss index 4b41dbc9bba0b..c555c0af2d077 100644 --- a/x-pack/plugins/graph/public/components/_search_bar.scss +++ b/x-pack/plugins/graph/public/components/_search_bar.scss @@ -1,7 +1,12 @@ .gphSearchBar__datasourceButton { - height: 100% !important; + max-width: 320px; + + @include euiBreakpoint('xs', 's') { + width: 100%; + max-width: none; + } } .gphSearchBar__datasourceButtonTooltip { padding: 0; -} \ No newline at end of file +} diff --git a/x-pack/plugins/graph/public/components/search_bar.test.tsx b/x-pack/plugins/graph/public/components/search_bar.test.tsx index c05da957599c9..559ff33330eab 100644 --- a/x-pack/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/plugins/graph/public/components/search_bar.test.tsx @@ -7,7 +7,7 @@ import { mountWithIntl } from '@kbn/test-jest-helpers'; import { SearchBar, SearchBarProps, SearchBarComponent, SearchBarStateProps } from './search_bar'; -import React, { Component, ReactElement } from 'react'; +import React, { Component } from 'react'; import { DocLinksStart, HttpStart, @@ -203,9 +203,7 @@ describe('search_bar', () => { // pick the button component out of the tree because // it's part of a popover and thus not covered by enzyme - ( - instance.find(QueryStringInput).prop('prepend') as ReactElement - ).props.children.props.onClick(); + instance.find('[data-test-subj="graphDatasourceButton"]').first().simulate('click'); expect(openSourceModal).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/graph/public/components/search_bar.tsx b/x-pack/plugins/graph/public/components/search_bar.tsx index 762a2e87d2a5a..046ed05977c79 100644 --- a/x-pack/plugins/graph/public/components/search_bar.tsx +++ b/x-pack/plugins/graph/public/components/search_bar.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiToolTip } from '@elastic/eui'; import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; @@ -110,6 +110,47 @@ export function SearchBarComponent(props: SearchBarStateProps & SearchBarProps) }} > + + + { + confirmWipeWorkspace( + () => + openSourceModal({ overlays, savedObjects, uiSettings }, onIndexPatternSelected), + i18n.translate('xpack.graph.clearWorkspace.confirmText', { + defaultMessage: + 'If you change data sources, your current fields and vertices will be reset.', + }), + { + confirmButtonText: i18n.translate( + 'xpack.graph.clearWorkspace.confirmButtonLabel', + { + defaultMessage: 'Change data source', + } + ), + title: i18n.translate('xpack.graph.clearWorkspace.modalTitle', { + defaultMessage: 'Unsaved changes', + }), + } + ); + }} + > + {currentIndexPattern + ? currentIndexPattern.title + : // This branch will be shown if the user exits the + // initial picker modal + i18n.translate('xpack.graph.bar.pickSourceLabel', { + defaultMessage: 'Select a data source', + })} + + + - { - confirmWipeWorkspace( - () => - openSourceModal( - { overlays, savedObjects, uiSettings }, - onIndexPatternSelected - ), - i18n.translate('xpack.graph.clearWorkspace.confirmText', { - defaultMessage: - 'If you change data sources, your current fields and vertices will be reset.', - }), - { - confirmButtonText: i18n.translate( - 'xpack.graph.clearWorkspace.confirmButtonLabel', - { - defaultMessage: 'Change data source', - } - ), - title: i18n.translate('xpack.graph.clearWorkspace.modalTitle', { - defaultMessage: 'Unsaved changes', - }), - } - ); - }} - > - {currentIndexPattern - ? currentIndexPattern.title - : // This branch will be shown if the user exits the - // initial picker modal - i18n.translate('xpack.graph.bar.pickSourceLabel', { - defaultMessage: 'Select a data source', - })} - - - } onChange={setQuery} /> diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 8ed33fb304525..e5a55322a2f10 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -20,6 +20,7 @@ "share", "presentationUtil", "dataViewFieldEditor", + "dataViewEditor", "expressionGauge", "expressionHeatmap", "eventAnnotation", diff --git a/x-pack/plugins/lens/public/app_plugin/app.scss b/x-pack/plugins/lens/public/app_plugin/app.scss index 99684a8b983c7..58ecce5592937 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.scss +++ b/x-pack/plugins/lens/public/app_plugin/app.scss @@ -10,10 +10,6 @@ flex-direction: column; height: 100%; overflow: hidden; - - > .kbnTopNavMenu__wrapper { - border-bottom: $euiBorderThin; - } } .lnsApp__frame { 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 bfad8dcd3f0ee..6e8cc4315ad8b 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -380,6 +380,75 @@ describe('Lens App', () => { }); }); + describe('TopNavMenu#dataViewPickerProps', () => { + it('calls the nav component with the correct dataview picker props if no permissions are given', async () => { + const { instance, lensStore } = await mountWith({ preloadedState: {} }); + const document = { + savedObjectId: defaultSavedObjectId, + state: { + query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], + }, + references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], + } as unknown as Document; + + act(() => { + lensStore.dispatch( + setState({ + query: 'fake query' as unknown as Query, + persistedDoc: document, + }) + ); + }); + instance.update(); + const props = instance + .find('[data-test-subj="lnsApp_topNav"]') + .prop('dataViewPickerComponentProps') as TopNavMenuData[]; + expect(props).toEqual( + expect.objectContaining({ + currentDataViewId: 'mockip', + onChangeDataView: expect.any(Function), + onDataViewCreated: expect.any(Function), + onAddField: undefined, + }) + ); + }); + + it('calls the nav component with the correct dataview picker props if permissions are given', async () => { + const { instance, lensStore, services } = await mountWith({ preloadedState: {} }); + services.dataViewFieldEditor.userPermissions.editIndexPattern = () => true; + const document = { + savedObjectId: defaultSavedObjectId, + state: { + query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], + }, + references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], + } as unknown as Document; + + act(() => { + lensStore.dispatch( + setState({ + query: 'fake query' as unknown as Query, + persistedDoc: document, + }) + ); + }); + instance.update(); + const props = instance + .find('[data-test-subj="lnsApp_topNav"]') + .prop('dataViewPickerComponentProps') as TopNavMenuData[]; + expect(props).toEqual( + expect.objectContaining({ + currentDataViewId: 'mockip', + onChangeDataView: expect.any(Function), + onDataViewCreated: expect.any(Function), + onAddField: expect.any(Function), + }) + ); + }); + }); + describe('persistence', () => { it('passes query and indexPatterns to TopNavMenu', async () => { const { instance, lensStore, services } = await mountWith({ preloadedState: {} }); 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 e532c82b7b3be..4ae1b8860c878 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 @@ -7,7 +7,7 @@ import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { useStore } from 'react-redux'; import { TopNavMenuData } from '@kbn/navigation-plugin/public'; import { downloadMultipleAs } from '@kbn/share-plugin/public'; @@ -16,6 +16,7 @@ import { exporters } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { trackUiEvent } from '../lens_ui_telemetry'; +import type { StateSetter } from '../types'; import { LensAppServices, LensTopNavActions, @@ -29,8 +30,15 @@ import { useLensDispatch, LensAppState, DispatchSetState, + updateDatasourceState, } from '../state_management'; -import { getIndexPatternsObjects, getIndexPatternsIds, getResolvedDateRange } from '../utils'; +import { + getIndexPatternsObjects, + getIndexPatternsIds, + getResolvedDateRange, + handleIndexPatternChange, + refreshIndexPatternsList, +} from '../utils'; import { combineQueryAndFilters, getLayerMetaInfo } from './show_underlying_data'; function getLensTopNavConfig(options: { @@ -222,6 +230,8 @@ export const LensTopNavMenu = ({ attributeService, discover, dashboardFeatureFlag, + dataViewFieldEditor, + dataViewEditor, dataViews, } = useKibana().services; @@ -232,7 +242,11 @@ export const LensTopNavMenu = ({ ); const [indexPatterns, setIndexPatterns] = useState([]); + const [currentIndexPattern, setCurrentIndexPattern] = useState(); const [rejectedIndexPatterns, setRejectedIndexPatterns] = useState([]); + const editPermission = dataViewFieldEditor.userPermissions.editIndexPattern(); + const closeFieldEditor = useRef<() => void | undefined>(); + const closeDataViewEditor = useRef<() => void | undefined>(); const { isSaveable, @@ -293,6 +307,20 @@ export const LensTopNavMenu = ({ dataViews, ]); + useEffect(() => { + if (indexPatterns.length > 0) { + setCurrentIndexPattern(indexPatterns[0]); + } + }, [indexPatterns]); + + useEffect(() => { + return () => { + // Make sure to close the editors when unmounting + closeFieldEditor.current?.(); + closeDataViewEditor.current?.(); + }; + }, []); + const { TopNavMenu } = navigation.ui; const { from, to } = data.query.timefilter.timefilter.getTime(); @@ -576,6 +604,123 @@ export const LensTopNavMenu = ({ }); }, [data.query.filterManager, data.query.queryString, dispatchSetState]); + const setDatasourceState: StateSetter = useMemo(() => { + return (updater) => { + dispatch( + updateDatasourceState({ + updater, + datasourceId: activeDatasourceId!, + clearStagedPreview: true, + }) + ); + }; + }, [activeDatasourceId, dispatch]); + + const refreshFieldList = useCallback(async () => { + if (currentIndexPattern && currentIndexPattern.id) { + refreshIndexPatternsList({ + activeDatasources: Object.keys(datasourceStates).reduce( + (acc, datasourceId) => ({ + ...acc, + [datasourceId]: datasourceMap[datasourceId], + }), + {} + ), + indexPatternId: currentIndexPattern.id, + setDatasourceState, + }); + } + // start a new session so all charts are refreshed + data.search.session.start(); + }, [ + currentIndexPattern, + data.search.session, + datasourceMap, + datasourceStates, + setDatasourceState, + ]); + + const editField = useMemo( + () => + editPermission + ? async (fieldName?: string, uiAction: 'edit' | 'add' = 'edit') => { + if (currentIndexPattern?.id) { + const indexPatternInstance = await data.dataViews.get(currentIndexPattern?.id); + closeFieldEditor.current = dataViewFieldEditor.openEditor({ + ctx: { + dataView: indexPatternInstance, + }, + fieldName, + onSave: async () => { + refreshFieldList(); + }, + }); + } + } + : undefined, + [editPermission, currentIndexPattern?.id, data.dataViews, dataViewFieldEditor, refreshFieldList] + ); + + const addField = useMemo( + () => (editPermission && editField ? () => editField(undefined, 'add') : undefined), + [editField, editPermission] + ); + + const createNewDataView = useCallback(() => { + const dataViewEditPermission = dataViewEditor.userPermissions.editDataView; + if (!dataViewEditPermission) { + return; + } + closeDataViewEditor.current = dataViewEditor.openEditor({ + onSave: async (dataView) => { + if (dataView.id) { + handleIndexPatternChange({ + activeDatasources: Object.keys(datasourceStates).reduce( + (acc, datasourceId) => ({ + ...acc, + [datasourceId]: datasourceMap[datasourceId], + }), + {} + ), + datasourceStates, + indexPatternId: dataView.id, + setDatasourceState, + }); + refreshFieldList(); + } + }, + }); + }, [dataViewEditor, datasourceMap, datasourceStates, refreshFieldList, setDatasourceState]); + + const dataViewPickerProps = { + trigger: { + label: currentIndexPattern?.title || '', + 'data-test-subj': 'lns-dataView-switch-link', + title: currentIndexPattern?.title || '', + }, + currentDataViewId: currentIndexPattern?.id, + onAddField: addField, + onDataViewCreated: createNewDataView, + onChangeDataView: (newIndexPatternId: string) => { + const currentDataView = indexPatterns.find( + (indexPattern) => indexPattern.id === newIndexPatternId + ); + setCurrentIndexPattern(currentDataView); + handleIndexPatternChange({ + activeDatasources: Object.keys(datasourceStates).reduce( + (acc, datasourceId) => ({ + ...acc, + [datasourceId]: datasourceMap[datasourceId], + }), + {} + ), + datasourceStates, + indexPatternId: newIndexPatternId, + setDatasourceState, + }); + }, + }; + return ( ip.isTimeBased()) || Boolean( @@ -607,6 +753,7 @@ export const LensTopNavMenu = ({ data-test-subj="lnsApp_topNav" screenTitle={'lens'} appName={'lens'} + displayStyle="detached" /> ); }; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index f7d865e92853e..6ddd49a7e5df0 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -84,6 +84,8 @@ export async function getLensServices( notifications: coreStart.notifications, savedObjectsClient: coreStart.savedObjects.client, presentationUtil: startDependencies.presentationUtil, + dataViewEditor: startDependencies.dataViewEditor, + dataViewFieldEditor: startDependencies.dataViewFieldEditor, dashboard: startDependencies.dashboard, getOriginatingAppName: () => { return embeddableEditorIncomingState?.originatingApp diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index 94507754b893f..abb6cfa6a06a6 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -30,6 +30,8 @@ import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public' import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import type { DashboardFeatureFlagConfig } from '@kbn/dashboard-plugin/public'; import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; +import type { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; +import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import { VisualizeFieldContext, ACTION_VISUALIZE_LENS_FIELD } from '@kbn/ui-actions-plugin/public'; import { ACTION_CONVERT_TO_LENS } from '@kbn/visualizations-plugin/public'; import type { EmbeddableEditorState, EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; @@ -140,6 +142,8 @@ export interface LensAppServices { // Temporarily required until the 'by value' paradigm is default. dashboardFeatureFlag: DashboardFeatureFlagConfig; + dataViewEditor: DataViewEditorStart; + dataViewFieldEditor: IndexPatternFieldEditorStart; } export interface LensTopNavTooltips { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx index 1baad07b2198c..ae087221fd49a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx @@ -7,8 +7,9 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; -import { EuiPopover, EuiPopoverTitle, EuiSelectable, EuiSelectableProps } from '@elastic/eui'; +import { EuiPopover, EuiPopoverTitle, EuiSelectableProps } from '@elastic/eui'; import { ToolbarButton, ToolbarButtonProps } from '@kbn/kibana-react-plugin/public'; +import { DataViewsList } from '@kbn/unified-search-plugin/public'; import { IndexPatternRef } from './types'; import { trackUiEvent } from '../lens_ui_telemetry'; @@ -67,50 +68,26 @@ export function ChangeIndexPattern({ isOpen={isPopoverOpen} closePopover={() => setPopoverIsOpen(false)} display="block" - panelPaddingSize="s" + panelPaddingSize="none" ownFocus >
- + {i18n.translate('xpack.lens.indexPattern.changeDataViewTitle', { defaultMessage: 'Data view', })} - - {...selectableProps} - searchable - singleSelection="always" - options={indexPatternRefs.map(({ title, id }) => ({ - key: id, - label: title, - value: id, - checked: id === indexPatternId ? 'on' : undefined, - }))} - onChange={(choices) => { - const choice = choices.find(({ checked }) => checked) as unknown as { - value: string; - }; + + { trackUiEvent('indexpattern_changed'); - onChangeIndexPattern(choice.value); + onChangeIndexPattern(newId); setPopoverIsOpen(false); }} - searchProps={{ - compressed: true, - ...(selectableProps ? selectableProps.searchProps : undefined), - }} - > - {(list, search) => ( - <> - {search} - {list} - - )} - + currentDataViewId={indexPatternId} + selectableProps={selectableProps} + />
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 512ef627c9116..9aaaf9c128a11 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -6,7 +6,6 @@ */ import React from 'react'; -import { waitFor } from '@testing-library/react'; import ReactDOM from 'react-dom'; import { createMockedDragDropContext } from './mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; @@ -19,7 +18,6 @@ import { act } from 'react-dom/test-utils'; import { coreMock } from '@kbn/core/public/mocks'; import { IndexPatternPrivateState } from './types'; import { mountWithIntl, shallowWithIntl } from '@kbn/test-jest-helpers'; -import { ChangeIndexPattern } from './change_indexpattern'; import { EuiProgress, EuiLoadingSpinner } from '@elastic/eui'; import { documentField } from './document_field'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; @@ -328,14 +326,6 @@ describe('IndexPattern Data Panel', () => { expect(wrapper.find('[data-test-subj="indexPattern-no-indexpatterns"]')).toHaveLength(1); }); - it('should call setState when the index pattern is switched', async () => { - const wrapper = shallowWithIntl(); - - wrapper.find(ChangeIndexPattern).prop('onChangeIndexPattern')('2'); - - expect(defaultProps.onChangeIndexPattern).toHaveBeenCalledWith('2'); - }); - describe('loading existence data', () => { function testProps() { const setState = jest.fn(); @@ -853,90 +843,5 @@ describe('IndexPattern Data Panel', () => { 'memory', ]); }); - describe('edit field list', () => { - beforeEach(() => { - props.indexPatternFieldEditor.userPermissions.editIndexPattern = () => true; - }); - it('should call field editor plugin on clicking add button', async () => { - const mockIndexPattern = {}; - (props.dataViews.get as jest.Mock).mockImplementation(() => - Promise.resolve(mockIndexPattern) - ); - const wrapper = mountWithIntl(); - act(() => { - const popoverTrigger = wrapper.find( - '[data-test-subj="lnsIndexPatternActions-popover"] button' - ); - popoverTrigger.simulate('click'); - }); - - wrapper.update(); - act(() => { - wrapper.find('[data-test-subj="indexPattern-add-field"]').first().simulate('click'); - }); - // wait for indx pattern to be loaded - await waitFor(() => { - expect(props.indexPatternFieldEditor.openEditor).toHaveBeenCalledWith( - expect.objectContaining({ - ctx: expect.objectContaining({ - dataView: mockIndexPattern, - }), - }) - ); - }); - }); - - it('should reload index pattern if callback gets called', async () => { - const mockIndexPattern = { - id: '1', - fields: [ - { - name: 'fieldOne', - aggregatable: true, - }, - ], - metaFields: [], - }; - (props.dataViews.get as jest.Mock).mockImplementation(() => - Promise.resolve(mockIndexPattern) - ); - const wrapper = mountWithIntl(); - - act(() => { - const popoverTrigger = wrapper.find( - '[data-test-subj="lnsIndexPatternActions-popover"] button' - ); - popoverTrigger.simulate('click'); - }); - - wrapper.update(); - act(() => { - wrapper.find('[data-test-subj="indexPattern-add-field"]').first().simulate('click'); - }); - - // wait for indx pattern to be loaded - await act(async () => await new Promise((r) => setTimeout(r, 0))); - - await (props.indexPatternFieldEditor.openEditor as jest.Mock).mock.calls[0][0].onSave(); - // wait for indx pattern to be loaded - await act(async () => await new Promise((r) => setTimeout(r, 0))); - expect(props.onUpdateIndexPattern).toHaveBeenCalledWith( - expect.objectContaining({ - fields: [ - expect.objectContaining({ - name: 'fieldOne', - }), - expect.anything(), - ], - }) - ); - }); - - it('should not render add button without permissions', () => { - props.indexPatternFieldEditor.userPermissions.editIndexPattern = () => false; - const wrapper = mountWithIntl(); - expect(wrapper.find('[data-test-subj="indexPattern-add-field"]').exists()).toBe(false); - }); - }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index ab437b9328e7e..d4cdca9a4c7fa 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -20,7 +20,6 @@ import { EuiFilterGroup, EuiFilterButton, EuiScreenReaderOnly, - EuiButtonIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { EsQueryConfig, Query, Filter } from '@kbn/es-query'; @@ -47,6 +46,8 @@ import { trackUiEvent } from '../lens_ui_telemetry'; import { loadIndexPatterns, syncExistingFields } from './loader'; import { fieldExists } from './pure_helpers'; import { Loader } from '../loader'; +import { LensFieldIcon } from './lens_field_icon'; +import { FieldGroups, FieldList } from './field_list'; export type Props = Omit, 'core'> & { data: DataPublicPluginStart; @@ -61,9 +62,6 @@ export type Props = Omit, 'co core: CoreStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; }; -import { LensFieldIcon } from './lens_field_icon'; -import { ChangeIndexPattern } from './change_indexpattern'; -import { FieldGroups, FieldList } from './field_list'; function sortFields(fieldA: IndexPatternField, fieldB: IndexPatternField) { return fieldA.displayName.localeCompare(fieldB.displayName, undefined, { sensitivity: 'base' }); @@ -573,11 +571,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ [currentIndexPattern.id, dataViews, editPermission, indexPatternFieldEditor, refreshFieldList] ); - const addField = useMemo( - () => (editPermission && editField ? () => editField(undefined, 'add') : undefined), - [editField, editPermission] - ); - const fieldProps = useMemo( () => ({ core, @@ -603,8 +596,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ ] ); - const [popoverOpen, setPopoverOpen] = useState(false); - return ( - - - - { - onChangeIndexPattern(newId); - clearLocalState(); - }} - /> - - {addField && ( - - { - setPopoverOpen(false); - }} - ownFocus - data-test-subj="lnsIndexPatternActions-popover" - button={ - { - setPopoverOpen(!popoverOpen); - }} - /> - } - > - { - setPopoverOpen(false); - addField(); - }} - > - {i18n.translate('xpack.lens.indexPatterns.addFieldButton', { - defaultMessage: 'Add field to data view', - })} - , - { - setPopoverOpen(false); - core.application.navigateToApp('management', { - path: `/kibana/indexPatterns/patterns/${currentIndexPattern.id}`, - }); - }} - > - {i18n.translate('xpack.lens.indexPatterns.manageFieldButton', { - defaultMessage: 'Manage data view fields', - })} - , - ]} - /> - - - )} - - { + handleChangeIndexPattern(indexPatternId, state, setState); + }, + + refreshIndexPatternsList: async ({ indexPatternId, setState }) => { + const newlyMappedIndexPattern = await loadIndexPatterns({ + indexPatternsService: dataViews, + cache: {}, + patterns: [indexPatternId], + }); + const indexPatternRefs = await dataViews.getIdsWithTitle(); + const indexPattern = newlyMappedIndexPattern[indexPatternId]; + setState((s) => { + return { + ...s, + indexPatterns: { + ...s.indexPatterns, + [indexPattern.id]: indexPattern, + }, + indexPatternRefs, + }; + }); + }, + // Reset the temporary invalid state when closing the editor, but don't // update the state if it's not needed updateStateOnCloseDimension: ({ state, layerId }) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index 91b9de58bdaa1..dba57f2fcb03e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -11,6 +11,7 @@ import { IndexPatternLayerPanelProps, LayerPanel } from './layerpanel'; import { shallowWithIntl as shallow } from '@kbn/test-jest-helpers'; import { ShallowWrapper } from 'enzyme'; import { EuiSelectable } from '@elastic/eui'; +import { DataViewsList } from '@kbn/unified-search-plugin/public'; import { ChangeIndexPattern } from './change_indexpattern'; import { getFieldByNameFactory } from './pure_helpers'; import { TermsIndexPatternColumn } from './operations'; @@ -212,7 +213,14 @@ describe('Layer Data Panel', () => { }); function getIndexPatternPickerList(instance: ShallowWrapper) { - return instance.find(ChangeIndexPattern).first().dive().find(EuiSelectable); + return instance + .find(ChangeIndexPattern) + .first() + .dive() + .find(DataViewsList) + .first() + .dive() + .find(EuiSelectable); } function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx index f8548321e49bd..efa1ef509b12d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.tsx @@ -22,7 +22,6 @@ export function LayerPanel({ state, layerId, onChangeIndexPattern }: IndexPatter const layer = state.layers[layerId]; const indexPattern = state.indexPatterns[layer.indexPatternId]; - const notFoundTitleLabel = i18n.translate('xpack.lens.layerPanel.missingDataView', { defaultMessage: 'Data view not found', }); diff --git a/x-pack/plugins/lens/public/mocks/datasource_mock.ts b/x-pack/plugins/lens/public/mocks/datasource_mock.ts index c30b39476b1ab..cf25828d3322c 100644 --- a/x-pack/plugins/lens/public/mocks/datasource_mock.ts +++ b/x-pack/plugins/lens/public/mocks/datasource_mock.ts @@ -36,6 +36,7 @@ export function createMockDatasource(id: string): DatasourceMock { initialize: jest.fn((_state?) => Promise.resolve()), renderDataPanel: jest.fn(), renderLayerPanel: jest.fn(), + getCurrentIndexPatternId: jest.fn(), toExpression: jest.fn((_frame, _state) => null), insertLayer: jest.fn((_state, _newLayerId) => ({})), removeLayer: jest.fn((_state, _layerId) => {}), diff --git a/x-pack/plugins/lens/public/mocks/services_mock.tsx b/x-pack/plugins/lens/public/mocks/services_mock.tsx index f5e94d374481a..800ec3dee25b1 100644 --- a/x-pack/plugins/lens/public/mocks/services_mock.tsx +++ b/x-pack/plugins/lens/public/mocks/services_mock.tsx @@ -10,6 +10,8 @@ import { Subject } from 'rxjs'; import { coreMock } from '@kbn/core/public/mocks'; import { navigationPluginMock } from '@kbn/navigation-plugin/public/mocks'; import { UI_SETTINGS } from '@kbn/data-plugin/public'; +import { indexPatternFieldEditorPluginMock } from '@kbn/data-view-field-editor-plugin/public/mocks'; +import { indexPatternEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks'; import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks'; import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks'; import { dashboardPluginMock } from '@kbn/dashboard-plugin/public/mocks'; @@ -155,5 +157,7 @@ export function makeDefaultServices( clear: jest.fn(), }, spaces: spacesPluginMock.createStartContract(), + dataViewFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), + dataViewEditor: indexPatternEditorPluginMock.createStartContract(), }; } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index b39c14cd82454..876cb63b0333d 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -32,6 +32,7 @@ import type { EventAnnotationPluginSetup } from '@kbn/event-annotation-plugin/pu import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; import { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; +import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; import { AppNavLinkStatus } from '@kbn/core/public'; import { @@ -123,6 +124,7 @@ export interface LensPluginStartDependencies { savedObjectsTagging?: SavedObjectTaggingPluginStart; presentationUtil: PresentationUtilPluginStart; dataViewFieldEditor: IndexPatternFieldEditorStart; + dataViewEditor: DataViewEditorStart; inspector: InspectorStartContract; spaces: SpacesPluginStart; usageCollection?: UsageCollectionStart; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 8c6c6d9af22dc..a91240e7e6a3e 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -223,6 +223,7 @@ export interface Datasource { // Given the current state, which parts should be saved? getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] }; + getCurrentIndexPatternId: (state: T) => string; insertLayer: (state: T, newLayerId: string) => T; removeLayer: (state: T, layerId: string) => T; @@ -274,6 +275,14 @@ export interface Datasource { state: T; }) => T | undefined; + updateCurrentIndexPatternId?: (props: { + indexPatternId: string; + state: T; + setState: StateSetter; + }) => void; + + refreshIndexPatternsList?: (props: { indexPatternId: string; setState: StateSetter }) => void; + toExpression: (state: T, layerId: string) => ExpressionAstExpression | string | null; getDatasourceSuggestionsForField: ( diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index b5ada350b2aaa..2a2bd0a35efa1 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -19,6 +19,7 @@ import type { LensBrushEvent, LensFilterEvent, Visualization, + StateSetter, } from './types'; import type { DatasourceStates, VisualizationState } from './state_management'; @@ -63,6 +64,43 @@ export const getInitialDatasourceId = (datasourceMap: DatasourceMap, doc?: Docum return (doc && getActiveDatasourceIdFromDoc(doc)) || Object.keys(datasourceMap)[0] || null; }; +export function handleIndexPatternChange({ + activeDatasources, + datasourceStates, + indexPatternId, + setDatasourceState, +}: { + activeDatasources: Record; + datasourceStates: DatasourceStates; + indexPatternId: string; + setDatasourceState: StateSetter; +}): void { + Object.entries(activeDatasources).forEach(([id, datasource]) => { + datasource?.updateCurrentIndexPatternId?.({ + state: datasourceStates[id].state, + indexPatternId, + setState: setDatasourceState, + }); + }); +} + +export function refreshIndexPatternsList({ + activeDatasources, + indexPatternId, + setDatasourceState, +}: { + activeDatasources: Record; + indexPatternId: string; + setDatasourceState: StateSetter; +}): void { + Object.entries(activeDatasources).forEach(([id, datasource]) => { + datasource?.refreshIndexPatternsList?.({ + indexPatternId, + setState: setDatasourceState, + }); + }); +} + export function getIndexPatternsIds({ activeDatasources, datasourceStates, @@ -70,17 +108,21 @@ export function getIndexPatternsIds({ activeDatasources: Record; datasourceStates: DatasourceStates; }): string[] { + let currentIndexPatternId: string | undefined; const references: SavedObjectReference[] = []; Object.entries(activeDatasources).forEach(([id, datasource]) => { const { savedObjectReferences } = datasource.getPersistableState(datasourceStates[id].state); + const indexPatternId = datasource.getCurrentIndexPatternId(datasourceStates[id].state); + currentIndexPatternId = indexPatternId; references.push(...savedObjectReferences); }); - - const uniqueFilterableIndexPatternIds = uniq( - references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) - ); - - return uniqueFilterableIndexPatternIds; + const referencesIds = references + .filter(({ type }) => type === 'index-pattern') + .map(({ id }) => id); + if (currentIndexPatternId) { + referencesIds.unshift(currentIndexPatternId); + } + return uniq(referencesIds); } export async function getIndexPatternsObjects( diff --git a/x-pack/plugins/lens/readme.md b/x-pack/plugins/lens/readme.md index c85005c09754e..84cea6feead06 100644 --- a/x-pack/plugins/lens/readme.md +++ b/x-pack/plugins/lens/readme.md @@ -14,8 +14,9 @@ Run all tests from the `x-pack` root directory - Unit tests: `yarn test:jest x-pack/plugins/lens` - Functional tests: - Run `node scripts/functional_tests_server` - - Run `node ../scripts/functional_test_runner.js --config ./test/functional/config.js --grep="lens app"` - - You may want to comment out all imports except for Lens in the config file. + - Run `node ../scripts/functional_test_runner.js --config ./test/functional/apps/lens/group1/config.ts` + - Run `node ../scripts/functional_test_runner.js --config ./test/functional/apps/lens/group2/config.ts` + - Run `node ../scripts/functional_test_runner.js --config ./test/functional/apps/lens/group3/config.ts` - API Functional tests: - Run `node scripts/functional_tests_server` - Run `node ../scripts/functional_test_runner.js --config ./test/api_integration/config.ts --grep=Lens` diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index 380d387249e17..e00581833f621 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -28,13 +28,14 @@ { "path": "../../../src/plugins/saved_objects/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../../../src/plugins/embeddable/tsconfig.json" }, - { "path": "../../../src/plugins/presentation_util/tsconfig.json" }, - { "path": "../../../src/plugins/field_formats/tsconfig.json" }, - { "path": "../../../src/plugins/chart_expressions/expression_xy/tsconfig.json" }, - { "path": "../../../src/plugins/chart_expressions/expression_heatmap/tsconfig.json" }, - { "path": "../../../src/plugins/chart_expressions/expression_gauge/tsconfig.json" }, - { "path": "../../../src/plugins/event_annotation/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json"}, + { "path": "../../../src/plugins/presentation_util/tsconfig.json"}, + { "path": "../../../src/plugins/field_formats/tsconfig.json"}, + { "path": "../../../src/plugins/chart_expressions/expression_heatmap/tsconfig.json"}, + { "path": "../../../src/plugins/chart_expressions/expression_gauge/tsconfig.json"}, + { "path": "../../../src/plugins/data_view_editor/tsconfig.json"}, + { "path": "../../../src/plugins/event_annotation/tsconfig.json"}, + { "path": "../../../src/plugins/chart_expressions/expression_xy/tsconfig.json"}, { "path": "../../../src/plugins/unified_search/tsconfig.json" } ] } diff --git a/x-pack/plugins/licensing/common/register_analytics_context_provider.test.ts b/x-pack/plugins/licensing/common/register_analytics_context_provider.test.ts new file mode 100644 index 0000000000000..7edccfd319c91 --- /dev/null +++ b/x-pack/plugins/licensing/common/register_analytics_context_provider.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { firstValueFrom, ReplaySubject, Subject } from 'rxjs'; +import type { ILicense } from './types'; +import { registerAnalyticsContextProvider } from './register_analytics_context_provider'; + +describe('registerAnalyticsContextProvider', () => { + const analyticsClientMock = { + registerContextProvider: jest.fn(), + }; + + let license$: Subject; + + beforeEach(() => { + jest.clearAllMocks(); + license$ = new ReplaySubject(1); + registerAnalyticsContextProvider(analyticsClientMock, license$); + }); + + test('should register the analytics context provider', () => { + expect(analyticsClientMock.registerContextProvider).toHaveBeenCalledTimes(1); + }); + + test('emits a context value the moment license emits', async () => { + license$.next({ + uid: 'uid', + status: 'active', + isActive: true, + type: 'basic', + signature: 'signature', + isAvailable: true, + toJSON: jest.fn(), + getUnavailableReason: jest.fn(), + hasAtLeast: jest.fn(), + check: jest.fn(), + getFeature: jest.fn(), + }); + await expect( + firstValueFrom(analyticsClientMock.registerContextProvider.mock.calls[0][0].context$) + ).resolves.toEqual({ + license_id: 'uid', + license_status: 'active', + license_type: 'basic', + }); + }); +}); diff --git a/x-pack/plugins/licensing/common/register_analytics_context_provider.ts b/x-pack/plugins/licensing/common/register_analytics_context_provider.ts new file mode 100644 index 0000000000000..60f3fbbb3e603 --- /dev/null +++ b/x-pack/plugins/licensing/common/register_analytics_context_provider.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Observable } from 'rxjs'; +import { map } from 'rxjs'; +import type { AnalyticsClient } from '@kbn/analytics-client'; +import type { ILicense } from './types'; + +export function registerAnalyticsContextProvider( + // Using `AnalyticsClient` from the package to be able to implement this method in the `common` dir. + analytics: Pick, + license$: Observable +) { + analytics.registerContextProvider({ + name: 'license info', + context$: license$.pipe( + map((license) => ({ + license_id: license.uid, + license_status: license.status, + license_type: license.type, + })) + ), + schema: { + license_id: { + type: 'keyword', + _meta: { description: 'The license ID', optional: true }, + }, + license_status: { + type: 'keyword', + _meta: { description: 'The license Status (active/invalid/expired)', optional: true }, + }, + license_type: { + type: 'keyword', + _meta: { + description: 'The license Type (basic/standard/gold/platinum/enterprise/trial)', + optional: true, + }, + }, + }, + }); +} diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index 9ef27e22657af..3953a29a08214 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -15,6 +15,7 @@ import { License } from '../common/license'; import { mountExpiredBanner } from './expired_banner'; import { FeatureUsageService } from './services'; import type { PublicLicenseJSON } from '../common/types'; +import { registerAnalyticsContextProvider } from '../common/register_analytics_context_provider'; export const licensingSessionStorageKey = 'xpack.licensing'; @@ -82,6 +83,8 @@ export class LicensingPlugin implements Plugin { if (license.isAvailable) { this.prevSignature = license.signature; diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 98dd1e7cbbb93..aaeeb4e058008 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -21,6 +21,7 @@ import { IClusterClient, } from '@kbn/core/server'; +import { registerAnalyticsContextProvider } from '../common/register_analytics_context_provider'; import { ILicense, PublicLicense, @@ -120,6 +121,8 @@ export class LicensingPlugin implements Plugin { const kibana = useMlKibana(); const { - services: { share, application }, + services: { data, share, application }, } = kibana; + const getMapsLink = async (anomaly: AnomaliesTableRecord) => { + const initialLayers = getInitialAnomaliesLayers(anomaly.jobId); + const anomalyBucketStartMoment = moment(anomaly.time).tz(getDateFormatTz()); + const anomalyBucketStart = anomalyBucketStartMoment.toISOString(); + const anomalyBucketEnd = anomalyBucketStartMoment + .add(anomaly.source.bucket_span, 'seconds') + .subtract(1, 'ms') + .toISOString(); + const timeRange = data.query.timefilter.timefilter.getTime(); + + // Set 'from' in timeRange to start bucket time for the specific anomaly + timeRange.from = anomalyBucketStart; + timeRange.to = anomalyBucketEnd; + + const locator = share.url.locators.get(MAPS_APP_LOCATOR); + const location = await locator?.getLocation({ + initialLayers, + timeRange, + ...(anomaly.entityName && anomaly.entityValue + ? { + query: { + language: SEARCH_QUERY_LANGUAGE.KUERY, + query: `${escapeKuery(anomaly.entityName)}:${escapeKuery(anomaly.entityValue)}`, + }, + } + : {}), + }); + return location; + }; + useEffect(() => { let unmounted = false; const discoverLocator = share.url.locators.get('DISCOVER_APP_LOCATOR'); @@ -561,23 +596,44 @@ export const LinksMenuUI = (props: LinksMenuProps) => { ); } - if (showViewSeriesLink === true && anomaly.isTimeSeriesViewRecord === true) { - items.push( - { - closePopover(); - viewSeries(); - }} - data-test-subj="mlAnomaliesListRowActionViewSeriesButton" - > - - - ); + if (showViewSeriesLink === true) { + if (anomaly.isTimeSeriesViewRecord) { + items.push( + { + closePopover(); + viewSeries(); + }} + data-test-subj="mlAnomaliesListRowActionViewSeriesButton" + > + + + ); + } + + if (anomaly.isGeoRecord === true) { + items.push( + { + const mapsLink = await getMapsLink(anomaly); + await application.navigateToApp(MAPS_APP_ID, { path: mapsLink?.path }); + }} + data-test-subj="mlAnomaliesListRowActionViewInMapsButton" + > + + + ); + } } if (application.capabilities.discover?.show && isCategorizationAnomalyRecord) { diff --git a/x-pack/plugins/ml/public/application/components/link_card/link_card.tsx b/x-pack/plugins/ml/public/application/components/link_card/link_card.tsx index ae5c4f0ab9ad9..3b3d6a2fdeb5c 100644 --- a/x-pack/plugins/ml/public/application/components/link_card/link_card.tsx +++ b/x-pack/plugins/ml/public/application/components/link_card/link_card.tsx @@ -39,7 +39,7 @@ export const LinkCard: FC = ({ onClick, href, isDisabled, - 'data-test-subj': dateTestSubj, + 'data-test-subj': dataTestSubj, }) => { const linkHrefAndOnClickProps = { ...(href ? { href } : {}), @@ -58,7 +58,7 @@ export const LinkCard: FC = ({ background: 'transparent', outline: 'none', }} - data-test-subj={dateTestSubj} + data-test-subj={dataTestSubj} color="subdued" {...linkHrefAndOnClickProps} > diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx index 4c1d8b478f971..9fcf0df33a0af 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx @@ -8,7 +8,11 @@ import React, { FC, Fragment, useState, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { ResultLink, FileDataVisualizerSpec } from '@kbn/data-visualizer-plugin/public'; +import type { + FileDataVisualizerSpec, + GetAdditionalLinksParams, + GetAdditionalLinks, +} from '@kbn/data-visualizer-plugin/public'; import { useTimefilter } from '../../contexts/kibana'; import { HelpMenu } from '../../components/help_menu'; import { useMlKibana, useMlLocator } from '../../contexts/kibana'; @@ -19,11 +23,6 @@ import { mlNodesAvailable, getMlNodeCount } from '../../ml_nodes_check/check_ml_ import { checkPermission } from '../../capabilities/check_capabilities'; import { MlPageHeader } from '../../components/page_header'; -interface GetUrlParams { - indexPatternId: string; - globalState: any; -} - export const FileDataVisualizerPage: FC = () => { useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); const { @@ -40,60 +39,62 @@ export const FileDataVisualizerPage: FC = () => { const [FileDataVisualizer, setFileDataVisualizer] = useState(null); - const links: ResultLink[] = useMemo( + const getAdditionalLinks: GetAdditionalLinks = useMemo( () => [ - { - id: 'create_ml_job', - title: i18n.translate('xpack.ml.fileDatavisualizer.actionsPanel.anomalyDetectionTitle', { - defaultMessage: 'Create new ML job', - }), - description: '', - icon: 'machineLearningApp', - type: 'file', - getUrl: async ({ indexPatternId, globalState }: GetUrlParams) => { - return await mlLocator.getUrl({ - page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, - pageState: { - index: indexPatternId, - globalState, - }, - }); - }, - canDisplay: async ({ indexPatternId }) => { - try { - const { timeFieldName } = await getDataView(indexPatternId); - return ( - isFullLicense() && - timeFieldName !== undefined && - checkPermission('canCreateJob') && - mlNodesAvailable() - ); - } catch (error) { - return false; - } + async ({ dataViewId, globalState }: GetAdditionalLinksParams) => [ + { + id: 'create_ml_job', + title: i18n.translate('xpack.ml.fileDatavisualizer.actionsPanel.anomalyDetectionTitle', { + defaultMessage: 'Create ML job', + }), + description: '', + icon: 'machineLearningApp', + type: 'file', + getUrl: async () => { + return await mlLocator.getUrl({ + page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, + pageState: { + index: dataViewId, + globalState, + }, + }); + }, + canDisplay: async () => { + try { + const { timeFieldName } = await getDataView(dataViewId); + return ( + isFullLicense() && + timeFieldName !== undefined && + checkPermission('canCreateJob') && + mlNodesAvailable() + ); + } catch (error) { + return false; + } + }, }, - }, - { - id: 'open_in_data_viz', - title: i18n.translate('xpack.ml.fileDatavisualizer.actionsPanel.dataframeTitle', { - defaultMessage: 'Open in Data Visualizer', - }), - description: '', - icon: 'dataVisualizer', - type: 'file', - getUrl: async ({ indexPatternId, globalState }: GetUrlParams) => { - return await mlLocator.getUrl({ - page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, - pageState: { - index: indexPatternId, - globalState, - }, - }); + { + id: 'open_in_data_viz', + title: i18n.translate('xpack.ml.fileDatavisualizer.actionsPanel.dataframeTitle', { + defaultMessage: 'Open in Data Visualizer', + }), + description: '', + icon: 'dataVisualizer', + type: 'file', + getUrl: async () => { + return await mlLocator.getUrl({ + page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, + pageState: { + index: dataViewId, + globalState, + }, + }); + }, + canDisplay: async () => dataViewId !== '', }, - canDisplay: async ({ indexPatternId }) => indexPatternId !== '', - }, + ], ], - [] + [mlLocator] ); useEffect(() => { @@ -114,7 +115,7 @@ export const FileDataVisualizerPage: FC = () => { defaultMessage="Data Visualizer" /> - + ) : null} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx index 5a041df220df9..ae5c7c9948f18 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx @@ -5,28 +5,38 @@ * 2.0. */ -import React, { FC, Fragment, useEffect, useState, useMemo } from 'react'; +import React, { FC, Fragment, useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { ResultLink, IndexDataVisualizerSpec } from '@kbn/data-visualizer-plugin/public'; +import type { + IndexDataVisualizerSpec, + ResultLink, + GetAdditionalLinks, + GetAdditionalLinksParams, +} from '@kbn/data-visualizer-plugin/public'; import { useMlKibana, useTimefilter, useMlLocator } from '../../contexts/kibana'; import { HelpMenu } from '../../components/help_menu'; import { ML_PAGES } from '../../../../common/constants/locator'; import { isFullLicense } from '../../license'; import { mlNodesAvailable, getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes'; import { checkPermission } from '../../capabilities/check_capabilities'; - import { MlPageHeader } from '../../components/page_header'; -interface GetUrlParams { - indexPatternId: string; - globalState: any; +interface RecognizerModule { + id: string; + title: string; + query: Record; + description: string; + logo: { + icon: string; + }; } export const IndexDataVisualizerPage: FC = () => { useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); const { services: { + http, docLinks, dataVisualizer, data: { @@ -48,8 +58,12 @@ export const IndexDataVisualizerPage: FC = () => { } }, []); - const links: ResultLink[] = useMemo( - () => [ + const getAsyncMLCards = async ({ + dataViewId, + dataViewTitle, + globalState, + }: GetAdditionalLinksParams): Promise => { + return [ { id: 'create_ml_ad_job', title: i18n.translate('xpack.ml.indexDatavisualizer.actionsPanel.anomalyDetectionTitle', { @@ -64,18 +78,18 @@ export const IndexDataVisualizerPage: FC = () => { ), icon: 'createAdvancedJob', type: 'file', - getUrl: async ({ indexPatternId, globalState }: GetUrlParams) => { + getUrl: async () => { return await mlLocator.getUrl({ page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED, pageState: { - index: indexPatternId, + index: dataViewId, globalState, }, }); }, - canDisplay: async ({ indexPatternId }) => { + canDisplay: async () => { try { - const { timeFieldName } = await getDataView(indexPatternId); + const { timeFieldName } = await getDataView(dataViewId); return ( isFullLicense() && timeFieldName !== undefined && @@ -86,7 +100,7 @@ export const IndexDataVisualizerPage: FC = () => { return false; } }, - dataTestSubj: 'dataVisualizerCreateAdvancedJobCard', + 'data-test-subj': 'dataVisualizerCreateAdvancedJobCard', }, { id: 'create_ml_dfa_job', @@ -101,11 +115,11 @@ export const IndexDataVisualizerPage: FC = () => { ), icon: 'classificationJob', type: 'file', - getUrl: async ({ indexPatternId, globalState }: GetUrlParams) => { + getUrl: async () => { return await mlLocator.getUrl({ page: ML_PAGES.DATA_FRAME_ANALYTICS_CREATE_JOB, pageState: { - index: indexPatternId, + index: dataViewId, globalState, }, }); @@ -115,12 +129,57 @@ export const IndexDataVisualizerPage: FC = () => { isFullLicense() && checkPermission('canCreateDataFrameAnalytics') && mlNodesAvailable() ); }, - dataTestSubj: 'dataVisualizerCreateDataFrameAnalyticsCard', + 'data-test-subj': 'dataVisualizerCreateDataFrameAnalyticsCard', }, - ], - [] - ); + ]; + }; + const getAsyncRecognizedModuleCards = async (params: GetAdditionalLinksParams) => { + const { dataViewId, dataViewTitle } = params; + const modules = await http.fetch( + `/api/ml/modules/recognize/${dataViewTitle}`, + { + method: 'GET', + } + ); + return modules?.map( + (m): ResultLink => ({ + id: m.id, + title: m.title, + description: m.description, + icon: m.logo.icon, + type: 'index', + getUrl: async () => { + return await mlLocator.getUrl({ + page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_RECOGNIZER, + pageState: { + id: m.id, + index: dataViewId, + }, + }); + }, + canDisplay: async () => { + try { + const { timeFieldName } = await getDataView(dataViewId); + return ( + isFullLicense() && + timeFieldName !== undefined && + checkPermission('canCreateJob') && + mlNodesAvailable() + ); + } catch (error) { + return false; + } + }, + 'data-test-subj': m.id, + }) + ); + }; + + const getAdditionalLinks: GetAdditionalLinks = useMemo( + () => [getAsyncRecognizedModuleCards, getAsyncMLCards], + [mlLocator] + ); return IndexDataVisualizer ? ( {IndexDataVisualizer !== null ? ( @@ -131,7 +190,7 @@ export const IndexDataVisualizerPage: FC = () => { defaultMessage="Data Visualizer" /> - + ) : null} 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 ddaaa55e7a3a4..d3dc2681cf4c5 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 @@ -16,7 +16,6 @@ import { EuiFlexItem, EuiIconTip, EuiToolTip, - htmlIdGenerator, } from '@elastic/eui'; import { @@ -36,15 +35,13 @@ import { MlTooltipComponent } from '../../components/chart_tooltip'; import { withKibana } from '@kbn/kibana-react-plugin/public'; import { useMlKibana } from '../../contexts/kibana'; import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types'; -import { AnomalySource } from '../../../maps/anomaly_source'; -import { CUSTOM_COLOR_RAMP } from '../../../maps/anomaly_layer_wizard_factory'; -import { LAYER_TYPE, APP_ID as MAPS_APP_ID } from '@kbn/maps-plugin/common'; +import { getInitialAnomaliesLayers } from '../../../maps/util'; +import { APP_ID as MAPS_APP_ID } from '@kbn/maps-plugin/common'; import { MAPS_APP_LOCATOR } from '@kbn/maps-plugin/public'; import { ExplorerChartsErrorCallOuts } from './explorer_charts_error_callouts'; import { addItemToRecentlyAccessed } from '../../util/recently_accessed'; import { EmbeddedMapComponentWrapper } from './explorer_chart_embedded_map'; import { useActiveCursor } from '@kbn/charts-plugin/public'; -import { ML_ANOMALY_LAYERS } from '../../../maps/util'; import { Chart, Settings } from '@elastic/charts'; import useObservable from 'react-use/lib/useObservable'; @@ -114,59 +111,7 @@ function ExplorerChartContainer({ const getMapsLink = useCallback(async () => { const { queryString, query } = getEntitiesQuery(series); - const initialLayers = []; - const typicalStyle = { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { - color: '#98A2B2', - }, - }, - lineColor: { - type: 'STATIC', - options: { - color: '#fff', - }, - }, - lineWidth: { - type: 'STATIC', - options: { - size: 2, - }, - }, - iconSize: { - type: 'STATIC', - options: { - size: 6, - }, - }, - }, - }; - - const style = { - type: 'VECTOR', - properties: { - fillColor: CUSTOM_COLOR_RAMP, - lineColor: CUSTOM_COLOR_RAMP, - }, - isTimeAware: false, - }; - - for (const layer in ML_ANOMALY_LAYERS) { - if (ML_ANOMALY_LAYERS.hasOwnProperty(layer)) { - initialLayers.push({ - id: htmlIdGenerator()(), - type: LAYER_TYPE.GEOJSON_VECTOR, - sourceDescriptor: AnomalySource.createDescriptor({ - jobId: series.jobId, - typicalActual: ML_ANOMALY_LAYERS[layer], - }), - style: ML_ANOMALY_LAYERS[layer] === ML_ANOMALY_LAYERS.TYPICAL ? typicalStyle : style, - }); - } - } + const initialLayers = getInitialAnomaliesLayers(series.jobId); const locator = share.url.locators.get(MAPS_APP_LOCATOR); const location = await locator.getLocation({ diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts index d2c7c3f1fe2d2..6aa3444c9caaf 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts @@ -19,6 +19,7 @@ import { } from '../../../common/constants/search'; import { EntityField, getEntityFieldList } from '../../../common/util/anomaly_utils'; import { extractErrorMessage } from '../../../common/util/errors'; +import { ML_JOB_AGGREGATION } from '../../../common/constants/aggregation_types'; import { isSourceDataChartableForDetector, isModelPlotChartableForDetector, @@ -495,6 +496,8 @@ export async function loadAnomaliesTableData( } anomaly.isTimeSeriesViewRecord = isChartable; + anomaly.isGeoRecord = + detector !== undefined && detector.function === ML_JOB_AGGREGATION.LAT_LONG; if (mlJobService.customUrlsByJob[jobId] !== undefined) { anomaly.customUrls = mlJobService.customUrlsByJob[jobId]; diff --git a/x-pack/plugins/ml/public/application/routing/ml_page_wrapper.tsx b/x-pack/plugins/ml/public/application/routing/ml_page_wrapper.tsx index f1674a12b77c6..4c2a6d0058edc 100644 --- a/x-pack/plugins/ml/public/application/routing/ml_page_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/routing/ml_page_wrapper.tsx @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import React, { FC } from 'react'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; diff --git a/x-pack/plugins/ml/public/application/routing/use_active_route.ts b/x-pack/plugins/ml/public/application/routing/use_active_route.ts index 925db8185c379..9183e45c3d0ae 100644 --- a/x-pack/plugins/ml/public/application/routing/use_active_route.ts +++ b/x-pack/plugins/ml/public/application/routing/use_active_route.ts @@ -8,11 +8,21 @@ import { useLocation, useRouteMatch } from 'react-router-dom'; import { keyBy } from 'lodash'; import { useMemo } from 'react'; +import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; +import { useMlKibana } from '../contexts/kibana'; import type { MlRoute } from './router'; +/** + * Provides an active route of the ML app. + * @param routesList + */ export const useActiveRoute = (routesList: MlRoute[]): MlRoute => { const { pathname } = useLocation(); + const { + services: { executionContext }, + } = useMlKibana(); + /** * Temp fix for routes with params. */ @@ -30,8 +40,14 @@ export const useActiveRoute = (routesList: MlRoute[]): MlRoute => { } // Remove trailing slash from the pathname const pathnameKey = pathname.replace(/\/$/, ''); - return routesMap[pathnameKey]; + return routesMap[pathnameKey] ?? routesMap['/overview']; }, [pathname]); - return activeRoute ?? routesMap['/overview']; + useExecutionContext(executionContext, { + name: 'Machine Learning', + type: 'application', + page: activeRoute?.path, + }); + + return activeRoute; }; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.tsx index 9f31f5777f9de..85350629263e4 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/embeddable_anomaly_charts_container.tsx @@ -11,6 +11,7 @@ import { Observable } from 'rxjs'; import { FormattedMessage } from '@kbn/i18n-react'; import { throttle } from 'lodash'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; +import { useEmbeddableExecutionContext } from '../common/use_embeddable_execution_context'; import { useAnomalyChartsInputResolver } from './use_anomaly_charts_input_resolver'; import type { IAnomalyChartsEmbeddable } from './anomaly_charts_embeddable'; import type { @@ -27,6 +28,7 @@ import { ANOMALY_THRESHOLD } from '../../../common'; import { TimeBuckets } from '../../application/util/time_buckets'; import { EXPLORER_ENTITY_FIELD_SELECTION_TRIGGER } from '../../ui_actions/triggers'; import { MlLocatorParams } from '../../../common/types/locator'; +import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE } from '..'; const RESIZE_THROTTLE_TIME_MS = 500; @@ -55,6 +57,13 @@ export const EmbeddableAnomalyChartsContainer: FC { + useEmbeddableExecutionContext( + services[0].executionContext, + embeddableInput, + ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, + id + ); + const [chartWidth, setChartWidth] = useState(0); const [severity, setSeverity] = useState( optionValueToThreshold( diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx index 06c400481491a..c354057d971bb 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx @@ -11,6 +11,7 @@ import { Observable } from 'rxjs'; import { CoreStart } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useEmbeddableExecutionContext } from '../common/use_embeddable_execution_context'; import { IAnomalySwimlaneEmbeddable } from './anomaly_swimlane_embeddable'; import { useSwimlaneInputResolver } from './swimlane_input_resolver'; import { SwimlaneType } from '../../application/explorer/explorer_constants'; @@ -22,6 +23,7 @@ import { AppStateSelectedCells } from '../../application/explorer/explorer_utils import { MlDependencies } from '../../application/app'; import { SWIM_LANE_SELECTION_TRIGGER } from '../../ui_actions'; import { + ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, AnomalySwimlaneEmbeddableInput, AnomalySwimlaneEmbeddableOutput, AnomalySwimlaneServices, @@ -52,6 +54,13 @@ export const EmbeddableSwimLaneContainer: FC = ( onLoading, onError, }) => { + useEmbeddableExecutionContext( + services[0].executionContext, + embeddableInput, + ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, + id + ); + const [chartWidth, setChartWidth] = useState(0); const [fromPage, setFromPage] = useState(1); diff --git a/x-pack/plugins/ml/public/embeddables/common/use_embeddable_execution_context.ts b/x-pack/plugins/ml/public/embeddables/common/use_embeddable_execution_context.ts new file mode 100644 index 0000000000000..68306c54c8590 --- /dev/null +++ b/x-pack/plugins/ml/public/embeddables/common/use_embeddable_execution_context.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 useObservable from 'react-use/lib/useObservable'; +import { map } from 'rxjs/operators'; +import { KibanaExecutionContext } from '@kbn/core/types'; +import { useMemo } from 'react'; +import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; +import type { Observable } from 'rxjs'; +import type { EmbeddableInput } from '@kbn/embeddable-plugin/common'; +import { ExecutionContextStart } from '@kbn/core/public'; + +/** + * Use execution context for ML embeddables. + * @param executionContext + * @param embeddableInput$ + * @param embeddableType + * @param id + */ +export function useEmbeddableExecutionContext( + executionContext: ExecutionContextStart, + embeddableInput$: Observable, + embeddableType: string, + id: string +) { + const parentExecutionContext = useObservable( + embeddableInput$.pipe(map((v) => v.executionContext)) + ); + + const embeddableExecutionContext: KibanaExecutionContext = useMemo(() => { + const child: KibanaExecutionContext = { + type: 'visualization', + name: embeddableType, + id, + }; + + return { + ...parentExecutionContext, + child, + }; + }, [parentExecutionContext, id]); + + useExecutionContext(executionContext, embeddableExecutionContext); +} diff --git a/x-pack/plugins/ml/public/locator/ml_locator.ts b/x-pack/plugins/ml/public/locator/ml_locator.ts index dcc31591208cb..b1ea2549c3347 100644 --- a/x-pack/plugins/ml/public/locator/ml_locator.ts +++ b/x-pack/plugins/ml/public/locator/ml_locator.ts @@ -77,6 +77,7 @@ export class MlLocatorDefinition implements LocatorDefinition { path = formatTrainedModelsNodesManagementUrl('', params.pageState); break; case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB: + case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_RECOGNIZER: case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED: case ML_PAGES.DATA_VISUALIZER: case ML_PAGES.DATA_VISUALIZER_FILE: diff --git a/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx b/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx index b40a08fadd128..6b9ee6c2e6332 100644 --- a/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx +++ b/x-pack/plugins/ml/public/maps/anomaly_layer_wizard_factory.tsx @@ -11,13 +11,13 @@ import type { StartServicesAccessor } from '@kbn/core/public'; import { LocatorPublic } from '@kbn/share-plugin/common'; import { SerializableRecord } from '@kbn/utility-types'; import type { LayerWizard, RenderWizardArguments } from '@kbn/maps-plugin/public'; -import { FIELD_ORIGIN, LAYER_TYPE, STYLE_TYPE } from '@kbn/maps-plugin/common'; +import { LAYER_TYPE } from '@kbn/maps-plugin/common'; import { VectorLayerDescriptor, VectorStylePropertiesDescriptor, } from '@kbn/maps-plugin/common/descriptor_types'; -import { SEVERITY_COLOR_RAMP } from '../../common'; import { ML_APP_LOCATOR, ML_PAGES } from '../../common/constants/locator'; +import { CUSTOM_COLOR_RAMP } from './util'; import { CreateAnomalySourceEditor } from './create_anomaly_source_editor'; import { AnomalySource, AnomalySourceDescriptor } from './anomaly_source'; @@ -26,17 +26,6 @@ import type { MlPluginStart, MlStartDependencies } from '../plugin'; import type { MlApiServices } from '../application/services/ml_api_service'; export const ML_ANOMALY = 'ML_ANOMALIES'; -export const CUSTOM_COLOR_RAMP = { - type: STYLE_TYPE.DYNAMIC, - options: { - customColorRamp: SEVERITY_COLOR_RAMP, - field: { - name: 'record_score', - origin: FIELD_ORIGIN.SOURCE, - }, - useCustomColorRamp: true, - }, -}; export class AnomalyLayerWizardFactory { public readonly type = ML_ANOMALY; diff --git a/x-pack/plugins/ml/public/maps/util.ts b/x-pack/plugins/ml/public/maps/util.ts index c1cb3f9efaead..88e9994d303a9 100644 --- a/x-pack/plugins/ml/public/maps/util.ts +++ b/x-pack/plugins/ml/public/maps/util.ts @@ -7,14 +7,19 @@ import { FeatureCollection, Feature, Geometry } from 'geojson'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { htmlIdGenerator } from '@elastic/eui'; +import { FIELD_ORIGIN, STYLE_TYPE } from '@kbn/maps-plugin/common'; import { fromKueryExpression, luceneStringToDsl, toElasticsearchQuery } from '@kbn/es-query'; import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; import { VectorSourceRequestMeta } from '@kbn/maps-plugin/common'; +import { LAYER_TYPE } from '@kbn/maps-plugin/common'; +import { SEVERITY_COLOR_RAMP } from '../../common'; import { formatHumanReadableDateTimeSeconds } from '../../common/util/date_utils'; import type { MlApiServices } from '../application/services/ml_api_service'; import { MLAnomalyDoc } from '../../common/types/anomalies'; import { SEARCH_QUERY_LANGUAGE } from '../../common/constants/search'; import { getIndexPattern } from '../application/explorer/reducers/explorer_reducer/get_index_pattern'; +import { AnomalySource } from './anomaly_source'; export const ML_ANOMALY_LAYERS = { TYPICAL: 'typical', @@ -22,6 +27,57 @@ export const ML_ANOMALY_LAYERS = { TYPICAL_TO_ACTUAL: 'typical to actual', } as const; +export const CUSTOM_COLOR_RAMP = { + type: STYLE_TYPE.DYNAMIC, + options: { + customColorRamp: SEVERITY_COLOR_RAMP, + field: { + name: 'record_score', + origin: FIELD_ORIGIN.SOURCE, + }, + useCustomColorRamp: true, + }, +}; + +export const ACTUAL_STYLE = { + type: 'VECTOR', + properties: { + fillColor: CUSTOM_COLOR_RAMP, + lineColor: CUSTOM_COLOR_RAMP, + }, + isTimeAware: false, +}; + +export const TYPICAL_STYLE = { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: '#98A2B2', + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 2, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, +}; + export type MlAnomalyLayersType = typeof ML_ANOMALY_LAYERS[keyof typeof ML_ANOMALY_LAYERS]; // Must reverse coordinates here. Map expects [lon, lat] - anomalies are stored as [lat, lon] for lat_lon jobs @@ -32,6 +88,27 @@ function getCoordinates(latLonString: string): number[] { .reverse(); } +export function getInitialAnomaliesLayers(jobId: string) { + const initialLayers = []; + for (const layer in ML_ANOMALY_LAYERS) { + if (ML_ANOMALY_LAYERS.hasOwnProperty(layer)) { + initialLayers.push({ + id: htmlIdGenerator()(), + type: LAYER_TYPE.GEOJSON_VECTOR, + sourceDescriptor: AnomalySource.createDescriptor({ + jobId, + typicalActual: ML_ANOMALY_LAYERS[layer as keyof typeof ML_ANOMALY_LAYERS], + }), + style: + ML_ANOMALY_LAYERS[layer as keyof typeof ML_ANOMALY_LAYERS] === ML_ANOMALY_LAYERS.TYPICAL + ? TYPICAL_STYLE + : ACTUAL_STYLE, + }); + } + } + return initialLayers; +} + export async function getResultsForJobId( mlResultsService: MlApiServices['results'], jobId: string, diff --git a/x-pack/plugins/observability/common/ui_settings_keys.ts b/x-pack/plugins/observability/common/ui_settings_keys.ts index 4c1b1dc729fea..287fe541cc7b6 100644 --- a/x-pack/plugins/observability/common/ui_settings_keys.ts +++ b/x-pack/plugins/observability/common/ui_settings_keys.ts @@ -5,6 +5,7 @@ * 2.0. */ +export const enableNewSyntheticsView = 'observability:enableNewSyntheticsView'; export const enableInspectEsQueries = 'observability:enableInspectEsQueries'; export const maxSuggestions = 'observability:maxSuggestions'; export const enableComparisonByDefault = 'observability:enableComparisonByDefault'; diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts b/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts index a09626654e6f8..de0127b08213e 100644 --- a/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts +++ b/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts @@ -16,6 +16,7 @@ import { useKibana } from '../utils/kibana_react'; export function useFetchRules({ searchText, ruleLastResponseFilter, + ruleStatusesFilter, typesFilter, setPage, page, @@ -43,6 +44,7 @@ export function useFetchRules({ searchText, typesFilter: typesFilter.length > 0 ? typesFilter : OBSERVABILITY_RULE_TYPES, ruleExecutionStatusesFilter: ruleLastResponseFilter, + ruleStatusesFilter, sort, }); setRulesState((oldState) => ({ @@ -58,6 +60,7 @@ export function useFetchRules({ const isFilterApplied = !( isEmpty(searchText) && isEmpty(ruleLastResponseFilter) && + isEmpty(ruleStatusesFilter) && isEmpty(typesFilter) ); @@ -66,7 +69,16 @@ export function useFetchRules({ setRulesState((oldState) => ({ ...oldState, isLoading: false, error: RULES_LOAD_ERROR })); } setInitialLoad(false); - }, [http, page, setPage, searchText, ruleLastResponseFilter, typesFilter, sort]); + }, [ + http, + page, + setPage, + searchText, + ruleLastResponseFilter, + ruleStatusesFilter, + typesFilter, + sort, + ]); useEffect(() => { fetchRules(); }, [fetchRules]); diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 19468ef0e2736..00db5b1873980 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -28,6 +28,7 @@ export { enableComparisonByDefault, enableInfrastructureView, enableServiceGroups, + enableNewSyntheticsView, } from '../common/ui_settings_keys'; export { uptimeOverviewLocatorID } from '../common'; diff --git a/x-pack/plugins/observability/public/observability_public_plugins_start.mock.ts b/x-pack/plugins/observability/public/observability_public_plugins_start.mock.ts index 930b75f578eb1..50836859a5415 100644 --- a/x-pack/plugins/observability/public/observability_public_plugins_start.mock.ts +++ b/x-pack/plugins/observability/public/observability_public_plugins_start.mock.ts @@ -38,6 +38,7 @@ const triggersActionsUiStartMock = { getAddAlertFlyout: jest.fn(), getRuleStatusDropdown: jest.fn(), getRuleTagBadge: jest.fn(), + getRuleStatusFilter: jest.fn(), ruleTypeRegistry: { has: jest.fn(), register: jest.fn(), diff --git a/x-pack/plugins/synthetics/public/apps/synthetics_app.tsx b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/index.ts similarity index 71% rename from x-pack/plugins/synthetics/public/apps/synthetics_app.tsx rename to x-pack/plugins/observability/public/pages/alerts/components/rule_stats/index.ts index 92531041157a7..b2b4e144952e0 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics_app.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/index.ts @@ -4,8 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; -export const SyntheticsApp = () => { - return
Synthetics App
; -}; +export { renderRuleStats } from './rule_stats'; +export type { RuleStatsState } from './types'; diff --git a/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.test.tsx b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.test.tsx new file mode 100644 index 0000000000000..6f2edf5d0b1b6 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.test.tsx @@ -0,0 +1,199 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderRuleStats } from './rule_stats'; +import { render, screen } from '@testing-library/react'; + +const RULES_PAGE_LINK = '/app/observability/alerts/rules'; +const STAT_CLASS = 'euiStat'; +const STAT_TITLE_PRIMARY_CLASS = 'euiStat__title--primary'; +const STAT_BUTTON_CLASS = 'euiButtonEmpty'; + +describe('Rule stats', () => { + test('renders all rule stats', async () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 0, + error: 0, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + expect(stats.length).toEqual(6); + }); + test('disabled stat is not clickable, when there are no disabled rules', async () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 0, + error: 0, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + const { findByText, container } = render(stats[4]); + const disabledElement = await findByText('Disabled'); + expect(disabledElement).toBeInTheDocument(); + expect(container.getElementsByClassName(STAT_CLASS).length).toBe(1); + expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(0); + expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(0); + }); + + test('disabled stat is clickable, when there are disabled rules', async () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 1, + muted: 0, + error: 0, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + const { container } = render(stats[4]); + expect(screen.getByText('Disabled').closest('a')).toHaveAttribute( + 'href', + `${RULES_PAGE_LINK}?_a=(lastResponse:!(),status:!(disabled))` + ); + + expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(1); + }); + + test('disabled stat count is link-colored, when there are disabled rules', async () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 1, + muted: 0, + error: 0, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + const { container } = render(stats[4]); + expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(1); + }); + + test('snoozed stat is not clickable, when there are no snoozed rules', async () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 0, + error: 0, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + const { findByText, container } = render(stats[3]); + const snoozedElement = await findByText('Snoozed'); + expect(snoozedElement).toBeInTheDocument(); + expect(container.getElementsByClassName(STAT_CLASS).length).toBe(1); + expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(0); + expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(0); + }); + + test('snoozed stat is clickable, when there are snoozed rules', () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 1, + error: 0, + snoozed: 1, + }, + RULES_PAGE_LINK, + false + ); + const { container } = render(stats[3]); + expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(1); + expect(screen.getByText('Snoozed').closest('a')).toHaveAttribute( + 'href', + `${RULES_PAGE_LINK}?_a=(lastResponse:!(),status:!(snoozed))` + ); + }); + + test('snoozed stat count is link-colored, when there are snoozed rules', async () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 1, + error: 0, + snoozed: 1, + }, + RULES_PAGE_LINK, + false + ); + const { container } = render(stats[3]); + expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(1); + }); + + test('errors stat is not clickable, when there are no error rules', async () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 0, + error: 0, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + const { findByText, container } = render(stats[2]); + const errorsElement = await findByText('Errors'); + expect(errorsElement).toBeInTheDocument(); + expect(container.getElementsByClassName(STAT_CLASS).length).toBe(1); + expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(0); + expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(0); + }); + + test('errors stat is clickable, when there are error rules', () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 0, + error: 2, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + const { container } = render(stats[2]); + expect(container.getElementsByClassName(STAT_BUTTON_CLASS).length).toBe(1); + expect(screen.getByText('Errors').closest('a')).toHaveAttribute( + 'href', + `${RULES_PAGE_LINK}?_a=(lastResponse:!(error),status:!())` + ); + }); + + test('errors stat count is link-colored, when there are error rules', () => { + const stats = renderRuleStats( + { + total: 11, + disabled: 0, + muted: 0, + error: 2, + snoozed: 0, + }, + RULES_PAGE_LINK, + false + ); + const { container } = render(stats[2]); + expect(container.getElementsByClassName(STAT_TITLE_PRIMARY_CLASS).length).toBe(1); + }); +}); diff --git a/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.tsx b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.tsx new file mode 100644 index 0000000000000..62c520c7b7442 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/rule_stats.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { EuiButtonEmpty, EuiStat } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; + +interface RuleStatsState { + total: number; + disabled: number; + muted: number; + error: number; + snoozed: number; +} +type StatType = 'disabled' | 'snoozed' | 'error'; + +const Divider = euiStyled.div` + border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; + height: 100%; +`; + +const StyledStat = euiStyled(EuiStat)` + .euiText { + line-height: 1; + } +`; + +const ConditionalWrap = ({ + condition, + wrap, + children, +}: { + condition: boolean; + wrap: (wrappedChildren: React.ReactNode) => JSX.Element; + children: JSX.Element; +}): JSX.Element => (condition ? wrap(children) : children); + +export const renderRuleStats = ( + ruleStats: RuleStatsState, + manageRulesHref: string, + ruleStatsLoading: boolean +) => { + const createRuleStatsLink = (stats: RuleStatsState, statType: StatType) => { + const count = stats[statType]; + let statsLink = `${manageRulesHref}?_a=(lastResponse:!(),status:!())`; + if (count > 0) { + switch (statType) { + case 'error': + statsLink = `${manageRulesHref}?_a=(lastResponse:!(error),status:!())`; + break; + case 'snoozed': + case 'disabled': + statsLink = `${manageRulesHref}?_a=(lastResponse:!(),status:!(${statType}))`; + break; + default: + break; + } + } + return statsLink; + }; + + const disabledStatsComponent = ( + 0} + wrap={(wrappedChildren) => ( + + {wrappedChildren} + + )} + > + 0 ? 'primary' : ''} + titleSize="xs" + isLoading={ruleStatsLoading} + data-test-subj="statDisabled" + /> + + ); + + const snoozedStatsComponent = ( + 0} + wrap={(wrappedChildren) => ( + + {wrappedChildren} + + )} + > + 0 ? 'primary' : ''} + titleSize="xs" + isLoading={ruleStatsLoading} + data-test-subj="statMuted" + /> + + ); + + const errorStatsComponent = ( + 0} + wrap={(wrappedChildren) => ( + + {wrappedChildren} + + )} + > + 0 ? 'primary' : ''} + titleSize="xs" + isLoading={ruleStatsLoading} + data-test-subj="statErrors" + /> + + ); + return [ + , + disabledStatsComponent, + snoozedStatsComponent, + errorStatsComponent, + , + + {i18n.translate('xpack.observability.alerts.manageRulesButtonLabel', { + defaultMessage: 'Manage Rules', + })} + , + ].reverse(); +}; diff --git a/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/types.ts b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/types.ts new file mode 100644 index 0000000000000..87ff668ebf87f --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/components/rule_stats/types.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface RuleStatsState { + total: number; + disabled: number; + muted: number; + error: number; + snoozed: number; +} + +export type StatType = 'disabled' | 'snoozed' | 'error'; diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx index e99a3195d0f30..2fe114771c329 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx @@ -5,15 +5,13 @@ * 2.0. */ -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { DataViewBase } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { ALERT_STATUS, AlertStatus } from '@kbn/rule-data-utils'; - -import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { loadRuleAggregations } from '@kbn/triggers-actions-ui-plugin/public'; import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields'; @@ -38,6 +36,7 @@ import { } from '../state_container'; import './styles.scss'; import { AlertsStatusFilter, AlertsDisclaimer, AlertsSearchBar } from '../../components'; +import { renderRuleStats } from '../../components/rule_stats'; import { ObservabilityAppServices } from '../../../../application/types'; import { OBSERVABILITY_RULE_TYPES } from '../../../rules/config'; @@ -57,11 +56,6 @@ export interface TopAlert { active: boolean; } -const Divider = euiStyled.div` - border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; - height: 100%; -`; - const regExpEscape = (str: string) => str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); const NO_INDEX_PATTERNS: DataViewBase[] = []; const BASE_ALERT_REGEX = new RegExp(`\\s*${regExpEscape(ALERT_STATUS)}\\s*:\\s*"(.*?|\\*?)"`); @@ -251,54 +245,7 @@ function AlertsPage() { ), - rightSideItems: [ - , - , - , - , - , - - {i18n.translate('xpack.observability.alerts.manageRulesButtonLabel', { - defaultMessage: 'Manage Rules', - })} - , - ].reverse(), + rightSideItems: renderRuleStats(ruleStats, manageRulesHref, ruleStatsLoading), }} > diff --git a/x-pack/plugins/observability/public/pages/rules/components/last_response_filter.tsx b/x-pack/plugins/observability/public/pages/rules/components/last_response_filter.tsx index d7154c47fecf7..f74c56f7f2792 100644 --- a/x-pack/plugins/observability/public/pages/rules/components/last_response_filter.tsx +++ b/x-pack/plugins/observability/public/pages/rules/components/last_response_filter.tsx @@ -18,12 +18,12 @@ import { } from '@elastic/eui'; import { RuleExecutionStatuses, RuleExecutionStatusValues } from '@kbn/alerting-plugin/common'; import { getHealthColor, rulesStatusesTranslationsMapping } from '../config'; -import { StatusFilterProps } from '../types'; +import { LastResponseFilterProps } from '../types'; -export const LastResponseFilter: React.FunctionComponent = ({ +export const LastResponseFilter: React.FunctionComponent = ({ selectedStatuses, onChange, -}: StatusFilterProps) => { +}: LastResponseFilterProps) => { const [selectedValues, setSelectedValues] = useState(selectedStatuses); const [isPopoverOpen, setIsPopoverOpen] = useState(false); diff --git a/x-pack/plugins/observability/public/pages/rules/config.ts b/x-pack/plugins/observability/public/pages/rules/config.ts index 9da846c2c1cc4..8c39acb75976d 100644 --- a/x-pack/plugins/observability/public/pages/rules/config.ts +++ b/x-pack/plugins/observability/public/pages/rules/config.ts @@ -7,7 +7,6 @@ import { RuleExecutionStatuses } from '@kbn/alerting-plugin/common'; import { Rule, RuleTypeIndex, RuleType } from '@kbn/triggers-actions-ui-plugin/public'; -import { Status, RuleStatus } from './types'; import { RULE_STATUS_OK, RULE_STATUS_ACTIVE, @@ -15,26 +14,8 @@ import { RULE_STATUS_PENDING, RULE_STATUS_UNKNOWN, RULE_STATUS_WARNING, - RULE_STATUS_ENABLED, - RULE_STATUS_DISABLED, - RULE_STATUS_SNOOZED_INDEFINITELY, } from './translations'; -export const statusMap: Status = { - [RuleStatus.enabled]: { - color: 'primary', - label: RULE_STATUS_ENABLED, - }, - [RuleStatus.disabled]: { - color: 'default', - label: RULE_STATUS_DISABLED, - }, - [RuleStatus.snoozed]: { - color: 'warning', - label: RULE_STATUS_SNOOZED_INDEFINITELY, - }, -}; - export const DEFAULT_SEARCH_PAGE_SIZE: number = 25; export function getHealthColor(status: RuleExecutionStatuses) { diff --git a/x-pack/plugins/observability/public/pages/rules/index.tsx b/x-pack/plugins/observability/public/pages/rules/index.tsx index 3d005c30fc747..a409754a51a14 100644 --- a/x-pack/plugins/observability/public/pages/rules/index.tsx +++ b/x-pack/plugins/observability/public/pages/rules/index.tsx @@ -23,6 +23,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { deleteRules, RuleTableItem, + RuleStatus, enableRule, disableRule, snoozeRule, @@ -82,7 +83,6 @@ function RulesPage() { application: { capabilities }, notifications: { toasts }, } = useKibana().services; - const { lastResponse, setLastResponse } = useRulesPageStateContainer(); const documentationLink = docLinks.links.observability.createAlerts; const ruleTypeRegistry = triggersActionsUi.ruleTypeRegistry; const canExecuteActions = hasExecuteActionsCapability(capabilities); @@ -93,8 +93,9 @@ function RulesPage() { }); const [inputText, setInputText] = useState(); const [searchText, setSearchText] = useState(); - // const [ruleLastResponseFilter, setRuleLastResponseFilter] = useState([]); const [typesFilter, setTypesFilter] = useState([]); + const { lastResponse, setLastResponse } = useRulesPageStateContainer(); + const { status, setStatus } = useRulesPageStateContainer(); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); const [rulesToDelete, setRulesToDelete] = useState([]); const [createRuleFlyoutVisibility, setCreateRuleFlyoutVisibility] = useState(false); @@ -110,6 +111,7 @@ function RulesPage() { const { rulesState, setRulesState, reload, noData, initialLoad } = useFetchRules({ searchText, ruleLastResponseFilter: lastResponse, + ruleStatusesFilter: status, typesFilter, page, setPage, @@ -287,6 +289,13 @@ function RulesPage() { [] ); + const setRuleStatusFilter = useCallback( + (ids: RuleStatus[]) => { + setStatus(ids); + }, + [setStatus] + ); + const setExecutionStatusFilter = useCallback( (ids: string[]) => { setLastResponse(ids); @@ -309,9 +318,6 @@ function RulesPage() { return ; } - // const nextSearchParams = new URLSearchParams(history.location.search); - // const xx = [...nextSearchParams.getAll('executionStatus')] || []; - // console.log(xx, '!!'); return ( <> @@ -353,6 +359,12 @@ function RulesPage() { onChange={setExecutionStatusFilter} />
+ + {triggersActionsUi.getRuleStatusFilter({ + selectedStatuses: status, + onChange: setRuleStatusFilter, + })} + (lastResponse: string[]) => RulesPageContainerState; + setStatus: (state: RulesPageContainerState) => (status: RuleStatus[]) => RulesPageContainerState; } const transitions: RulesPageStateTransitions = { @@ -39,6 +43,20 @@ const transitions: RulesPageStateTransitions = { }); return { ...state, lastResponse: filteredIds }; }, + setStatus: (state) => (status) => { + const filteredIds = status; + status.forEach((id) => { + const isPreviouslyChecked = state.status.includes(id); + if (!isPreviouslyChecked) { + filteredIds.concat(id); + } else { + filteredIds.filter((val) => { + return val !== id; + }); + } + }); + return { ...state, status: filteredIds }; + }, }; const rulesPageStateContainer = createStateContainer(defaultState, transitions); diff --git a/x-pack/plugins/observability/public/pages/rules/state_container/use_rules_page_state_container.tsx b/x-pack/plugins/observability/public/pages/rules/state_container/use_rules_page_state_container.tsx index 6b44dc8ae31d5..cd20de3f95c29 100644 --- a/x-pack/plugins/observability/public/pages/rules/state_container/use_rules_page_state_container.tsx +++ b/x-pack/plugins/observability/public/pages/rules/state_container/use_rules_page_state_container.tsx @@ -27,12 +27,14 @@ export function useRulesPageStateContainer() { useUrlStateSyncEffect(stateContainer); - const { setLastResponse } = stateContainer.transitions; - const { lastResponse } = useContainerSelector(stateContainer, (state) => state); + const { setLastResponse, setStatus } = stateContainer.transitions; + const { lastResponse, status } = useContainerSelector(stateContainer, (state) => state); return { lastResponse, + status, setLastResponse, + setStatus, }; } diff --git a/x-pack/plugins/observability/public/pages/rules/translations.ts b/x-pack/plugins/observability/public/pages/rules/translations.ts index 36f8ff62f1a4c..69f0b5beebf46 100644 --- a/x-pack/plugins/observability/public/pages/rules/translations.ts +++ b/x-pack/plugins/observability/public/pages/rules/translations.ts @@ -53,27 +53,6 @@ export const RULE_STATUS_WARNING = i18n.translate( } ); -export const RULE_STATUS_ENABLED = i18n.translate( - 'xpack.observability.rules.rulesTable.ruleStatusEnabled', - { - defaultMessage: 'Enabled', - } -); - -export const RULE_STATUS_DISABLED = i18n.translate( - 'xpack.observability.rules.rulesTable.ruleStatusDisabled', - { - defaultMessage: 'Disabled', - } -); - -export const RULE_STATUS_SNOOZED_INDEFINITELY = i18n.translate( - 'xpack.observability.rules.rulesTable.ruleStatusSnoozedIndefinitely', - { - defaultMessage: 'Snoozed indefinitely', - } -); - export const LAST_RESPONSE_COLUMN_TITLE = i18n.translate( 'xpack.observability.rules.rulesTable.columns.lastResponseTitle', { diff --git a/x-pack/plugins/observability/public/pages/rules/types.ts b/x-pack/plugins/observability/public/pages/rules/types.ts index 8b3e337b99bd8..866e5de91d9c5 100644 --- a/x-pack/plugins/observability/public/pages/rules/types.ts +++ b/x-pack/plugins/observability/public/pages/rules/types.ts @@ -7,38 +7,9 @@ import { Dispatch, SetStateAction } from 'react'; import { EuiTableSortingType, EuiBasicTableColumn } from '@elastic/eui'; import { RuleExecutionStatus } from '@kbn/alerting-plugin/common'; -import { RuleTableItem, Rule } from '@kbn/triggers-actions-ui-plugin/public'; -export interface StatusProps { - type: RuleStatus; - disabled: boolean; - onClick: () => void; -} - -export enum RuleStatus { - enabled = 'enabled', - disabled = 'disabled', - snoozed = 'snoozed', -} - -export type Status = Record< - RuleStatus, - { - color: string; - label: string; - } ->; - -export interface StatusContextProps { - item: RuleTableItem; - disabled: boolean; - onStatusChanged: (status: RuleStatus) => void; - enableRule: (rule: Rule) => Promise; - disableRule: (rule: Rule) => Promise; - muteRule: (rule: Rule) => Promise; - unMuteRule: (rule: Rule) => Promise; -} +import { RuleTableItem, Rule, RuleStatus } from '@kbn/triggers-actions-ui-plugin/public'; -export interface StatusFilterProps { +export interface LastResponseFilterProps { selectedStatuses: string[]; onChange?: (selectedRuleStatusesIds: string[]) => void; } @@ -71,6 +42,7 @@ export interface Pagination { export interface FetchRulesProps { searchText: string | undefined; ruleLastResponseFilter: string[]; + ruleStatusesFilter: RuleStatus[]; typesFilter: string[]; page: Pagination; setPage: Dispatch>; diff --git a/x-pack/plugins/observability/public/services/call_observability_api/index.ts b/x-pack/plugins/observability/public/services/call_observability_api/index.ts index 881ea80a4de47..f76f9f5cd7e07 100644 --- a/x-pack/plugins/observability/public/services/call_observability_api/index.ts +++ b/x-pack/plugins/observability/public/services/call_observability_api/index.ts @@ -5,9 +5,7 @@ * 2.0. */ -// @ts-expect-error -import { formatRequest } from '@kbn/server-route-repository/target_node/format_request'; -import type { formatRequest as formatRequestType } from '@kbn/server-route-repository/target_types/format_request'; +import { formatRequest } from '@kbn/server-route-repository'; import type { HttpSetup } from '@kbn/core/public'; import type { AbstractObservabilityClient, ObservabilityClient } from './types'; @@ -19,9 +17,7 @@ export function createCallObservabilityApi(http: HttpSetup) { const client: AbstractObservabilityClient = (endpoint, options) => { const { params: { path, body, query } = {}, ...rest } = options; - const { method, pathname } = formatRequest(endpoint, path) as ReturnType< - typeof formatRequestType - >; + const { method, pathname } = formatRequest(endpoint, path); return http[method](pathname, { ...rest, diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index 02c519b10d19c..5b21b07d1cea3 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -18,6 +18,7 @@ import { apmProgressiveLoading, enableServiceGroups, apmServiceInventoryOptimizedSorting, + enableNewSyntheticsView, } from '../common/ui_settings_keys'; const technicalPreviewLabel = i18n.translate( @@ -31,6 +32,22 @@ const technicalPreviewLabel = i18n.translate( * uiSettings definitions for Observability. */ export const uiSettings: Record> = { + [enableNewSyntheticsView]: { + category: [observabilityFeatureId], + name: i18n.translate('xpack.observability.enableNewSyntheticsViewExperimentName', { + defaultMessage: 'Enable new synthetic monitoring application', + }), + value: false, + description: i18n.translate( + 'xpack.observability.enableNewSyntheticsViewExperimentDescription', + { + defaultMessage: + 'Enable new synthetic monitoring application in observability. Refresh the page to apply the setting.', + } + ), + schema: schema.boolean(), + requiresPageReload: true, + }, [enableInspectEsQueries]: { category: [observabilityFeatureId], name: i18n.translate('xpack.observability.enableInspectEsQueriesExperimentName', { @@ -71,7 +88,7 @@ export const uiSettings: Record --dir ../../test/osquery_cypress/es_archives --config ../../../test/functional/config.js --es-url http://:@: +node ../../../scripts/es_archiver save --dir ../../test/osquery_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http://:@: ``` Example: ```sh -node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/osquery_cypress/es_archives --config ../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220 +node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/osquery_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http://elastic:changeme@localhost:9220 ``` Note that the command will create the folder if it does not exist. diff --git a/x-pack/plugins/osquery/public/plugin.ts b/x-pack/plugins/osquery/public/plugin.ts index e88a866cc545f..e21defbaa8828 100644 --- a/x-pack/plugins/osquery/public/plugin.ts +++ b/x-pack/plugins/osquery/public/plugin.ts @@ -45,6 +45,7 @@ export class OsqueryPlugin implements Plugin; }; - aggs?: { - [x: string]: BucketAggsSchemas; - }; + aggs?: BucketAggsSchemas; + aggregations?: BucketAggsSchemas; } /** @@ -114,78 +113,83 @@ interface BucketAggsSchemas { * - significant_text * - variable_width_histogram */ -export const BucketAggsSchemas: t.Type = t.recursion('BucketAggsSchemas', () => - t.exact( - t.partial({ - filter: t.exact( - t.partial({ - term: t.record(t.string, t.union([t.string, t.boolean, t.number])), - }) - ), - date_histogram: t.exact( - t.partial({ - field: t.string, - fixed_interval: t.string, - min_doc_count: t.number, - extended_bounds: t.type({ - min: t.string, - max: t.string, - }), - }) - ), - histogram: t.exact( - t.partial({ - field: t.string, - interval: t.number, - min_doc_count: t.number, - extended_bounds: t.exact( - t.type({ - min: t.number, - max: t.number, - }) - ), - hard_bounds: t.exact( - t.type({ - min: t.number, - max: t.number, - }) - ), - missing: t.number, - keyed: t.boolean, - order: t.exact( - t.type({ - _count: t.string, - _key: t.string, - }) - ), - }) - ), - nested: t.type({ - path: t.string, - }), - terms: t.exact( - t.partial({ - field: t.string, - collect_mode: t.string, - exclude: t.union([t.string, t.array(t.string)]), - include: t.union([t.string, t.array(t.string)]), - execution_hint: t.string, - missing: t.union([t.number, t.string]), - min_doc_count: t.number, - size: t.number, - show_term_doc_count_error: t.boolean, - order: t.union([ - sortOrderSchema, - t.record(t.string, sortOrderSchema), - t.array(t.record(t.string, sortOrderSchema)), - ]), - }) - ), - aggs: t.record(t.string, BucketAggsSchemas), - }) - ) +const bucketAggsTempsSchemas: t.Type = t.exact( + t.partial({ + filter: t.exact( + t.partial({ + term: t.record(t.string, t.union([t.string, t.boolean, t.number])), + }) + ), + date_histogram: t.exact( + t.partial({ + field: t.string, + fixed_interval: t.string, + min_doc_count: t.number, + extended_bounds: t.type({ + min: t.string, + max: t.string, + }), + }) + ), + histogram: t.exact( + t.partial({ + field: t.string, + interval: t.number, + min_doc_count: t.number, + extended_bounds: t.exact( + t.type({ + min: t.number, + max: t.number, + }) + ), + hard_bounds: t.exact( + t.type({ + min: t.number, + max: t.number, + }) + ), + missing: t.number, + keyed: t.boolean, + order: t.exact( + t.type({ + _count: t.string, + _key: t.string, + }) + ), + }) + ), + nested: t.type({ + path: t.string, + }), + terms: t.exact( + t.partial({ + field: t.string, + collect_mode: t.string, + exclude: t.union([t.string, t.array(t.string)]), + include: t.union([t.string, t.array(t.string)]), + execution_hint: t.string, + missing: t.union([t.number, t.string]), + min_doc_count: t.number, + size: t.number, + show_term_doc_count_error: t.boolean, + order: t.union([ + sortOrderSchema, + t.record(t.string, sortOrderSchema), + t.array(t.record(t.string, sortOrderSchema)), + ]), + }) + ), + }) ); +export const bucketAggsSchemas = t.intersection([ + bucketAggsTempsSchemas, + t.partial({ + aggs: t.union([t.record(t.string, bucketAggsTempsSchemas), t.undefined]), + aggregations: t.union([t.record(t.string, bucketAggsTempsSchemas), t.undefined]), + }), +]); + /** * Schemas for the metrics Aggregations * @@ -215,57 +219,75 @@ export const BucketAggsSchemas: t.Type = t.recursion('BucketA * - t_test * - value_count */ -export const metricsAggsSchemas = t.partial({ - avg: t.partial({ - field: t.string, - missing: t.union([t.string, t.number, t.boolean]), - }), - cardinality: t.partial({ - field: t.string, - precision_threshold: t.number, - rehash: t.boolean, - missing: t.union([t.string, t.number, t.boolean]), - }), - min: t.partial({ - field: t.string, - missing: t.union([t.string, t.number, t.boolean]), - format: t.string, - }), - max: t.partial({ - field: t.string, - missing: t.union([t.string, t.number, t.boolean]), - format: t.string, - }), - sum: t.partial({ - field: t.string, - missing: t.union([t.string, t.number, t.boolean]), - }), - top_hits: t.partial({ - explain: t.boolean, - docvalue_fields: t.union([t.string, t.array(t.string)]), - stored_fields: t.union([t.string, t.array(t.string)]), - from: t.number, - size: t.number, - sort: sortSchema, - seq_no_primary_term: t.boolean, - version: t.boolean, - track_scores: t.boolean, - highlight: t.any, - _source: t.union([t.boolean, t.string, t.array(t.string)]), - }), - weighted_avg: t.partial({ - format: t.string, - value_type: t.string, - value: t.partial({ - field: t.string, - missing: t.number, - }), - weight: t.partial({ - field: t.string, - missing: t.number, - }), - }), -}); +export const metricsAggsSchemas = t.exact( + t.partial({ + avg: t.exact( + t.partial({ + field: t.string, + missing: t.union([t.string, t.number, t.boolean]), + }) + ), + cardinality: t.exact( + t.partial({ + field: t.string, + precision_threshold: t.number, + rehash: t.boolean, + missing: t.union([t.string, t.number, t.boolean]), + }) + ), + min: t.exact( + t.partial({ + field: t.string, + missing: t.union([t.string, t.number, t.boolean]), + format: t.string, + }) + ), + max: t.exact( + t.partial({ + field: t.string, + missing: t.union([t.string, t.number, t.boolean]), + format: t.string, + }) + ), + sum: t.exact( + t.partial({ + field: t.string, + missing: t.union([t.string, t.number, t.boolean]), + }) + ), + top_hits: t.exact( + t.partial({ + explain: t.boolean, + docvalue_fields: t.union([t.string, t.array(t.string)]), + stored_fields: t.union([t.string, t.array(t.string)]), + from: t.number, + size: t.number, + sort: sortSchema, + seq_no_primary_term: t.boolean, + version: t.boolean, + track_scores: t.boolean, + highlight: t.any, + _source: t.union([t.boolean, t.string, t.array(t.string)]), + }) + ), + weighted_avg: t.exact( + t.partial({ + format: t.string, + value_type: t.string, + value: t.partial({ + field: t.string, + missing: t.number, + }), + weight: t.partial({ + field: t.string, + missing: t.number, + }), + }) + ), + aggs: t.undefined, + aggregations: t.undefined, + }) +); export type PutIndexTemplateRequest = estypes.IndicesPutIndexTemplateRequest & { body?: { composed_of?: string[] }; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index b5df5dc830d48..effd2f4f81b89 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -647,7 +647,6 @@ export class AlertsClient { [ReadOperations.Find, ReadOperations.Get, WriteOperations.Update], AlertingAuthorizationEntity.Alert ); - // As long as the user can read a minimum of one type of rule type produced by the provided feature, // the user should be provided that features' alerts index. // Limiting which alerts that user can read on that index will be done via the findAuthorizationFilter @@ -655,19 +654,16 @@ export class AlertsClient { for (const ruleType of augmentedRuleTypes.authorizedRuleTypes) { authorizedFeatures.add(ruleType.producer); } - const validAuthorizedFeatures = Array.from(authorizedFeatures).filter( (feature): feature is ValidFeatureId => featureIds.includes(feature) && isValidFeatureId(feature) ); - - const toReturn = validAuthorizedFeatures.flatMap((feature) => { - const indices = this.ruleDataService.findIndicesByFeature(feature, Dataset.alerts); - if (feature === 'siem') { - return indices.map((i) => `${i.baseName}-${this.spaceId}`); - } else { - return indices.map((i) => i.baseName); + const toReturn = validAuthorizedFeatures.map((feature) => { + const index = this.ruleDataService.findIndexByFeature(feature, Dataset.alerts); + if (index == null) { + throw new Error(`This feature id ${feature} should be associated to an alert index`); } + return index?.getPrimaryAlias(this.spaceId ?? '*') ?? ''; }); return toReturn; diff --git a/x-pack/plugins/rule_registry/server/routes/find.ts b/x-pack/plugins/rule_registry/server/routes/find.ts index eca0a6c2a0551..675037baa312a 100644 --- a/x-pack/plugins/rule_registry/server/routes/find.ts +++ b/x-pack/plugins/rule_registry/server/routes/find.ts @@ -14,7 +14,7 @@ import { PositiveInteger } from '@kbn/securitysolution-io-ts-types'; import { RacRequestHandlerContext } from '../types'; import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; import { buildRouteValidation } from './utils/route_validation'; -import { BucketAggsSchemas } from '../../common/types'; +import { bucketAggsSchemas, metricsAggsSchemas } from '../../common/types'; export const findAlertsByQueryRoute = (router: IRouter) => { router.post( @@ -26,7 +26,11 @@ export const findAlertsByQueryRoute = (router: IRouter t.partial({ index: t.string, query: t.object, - aggs: t.union([t.record(t.string, BucketAggsSchemas), t.undefined]), + aggs: t.union([ + t.record(t.string, bucketAggsSchemas), + t.record(t.string, metricsAggsSchemas), + t.undefined, + ]), size: t.union([PositiveInteger, t.undefined]), track_total_hits: t.union([t.boolean, t.undefined]), _source: t.union([t.array(t.string), t.undefined]), diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts index 8bbc14cab9f82..cfbfafd0092bf 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts @@ -16,7 +16,7 @@ export const ruleDataServiceMock = { initializeService: jest.fn(), initializeIndex: jest.fn(), findIndexByName: jest.fn(), - findIndicesByFeature: jest.fn(), + findIndexByFeature: jest.fn(), }), }; diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts index a940201841e6f..a336f73c87c79 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts @@ -71,7 +71,7 @@ export interface IRuleDataService { * Looks up the index information associated with the given Kibana "feature". * Note: features are used in RBAC. */ - findIndicesByFeature(featureId: ValidFeatureId, dataset?: Dataset): IndexInfo[]; + findIndexByFeature(featureId: ValidFeatureId, dataset: Dataset): IndexInfo | null; } // TODO: This is a leftover. Remove its usage from the "observability" plugin and delete it. @@ -214,8 +214,13 @@ export class RuleDataService implements IRuleDataService { return this.indicesByBaseName.get(baseName) ?? null; } - public findIndicesByFeature(featureId: ValidFeatureId, dataset?: Dataset): IndexInfo[] { + public findIndexByFeature(featureId: ValidFeatureId, dataset: Dataset): IndexInfo | null { const foundIndices = this.indicesByFeatureId.get(featureId) ?? []; - return dataset ? foundIndices.filter((i) => i.indexOptions.dataset === dataset) : foundIndices; + if (dataset && foundIndices.length > 0) { + return foundIndices.filter((i) => i.indexOptions.dataset === dataset)[0]; + } else if (foundIndices.length > 0) { + return foundIndices[0]; + } + return null; } } diff --git a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts index 79b9a8cd75c58..9bfc4d7a40640 100644 --- a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts +++ b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.test.ts @@ -78,12 +78,10 @@ describe('ruleRegistrySearchStrategyProvider()', () => { const searchStrategySearch = jest.fn().mockImplementation(() => of(response)); beforeEach(() => { - ruleDataService.findIndicesByFeature.mockImplementation(() => { - return [ - { - baseName: 'test', - } as IndexInfo, - ]; + ruleDataService.findIndexByFeature.mockImplementation(() => { + return { + baseName: 'test', + } as IndexInfo; }); data.search.getSearchStrategy.mockImplementation(() => { @@ -108,7 +106,7 @@ describe('ruleRegistrySearchStrategyProvider()', () => { }); afterEach(() => { - ruleDataService.findIndicesByFeature.mockClear(); + ruleDataService.findIndexByFeature.mockClear(); data.search.getSearchStrategy.mockClear(); (data.search.searchAsInternalUser.search as jest.Mock).mockClear(); getAuthzFilterSpy.mockClear(); @@ -156,12 +154,10 @@ describe('ruleRegistrySearchStrategyProvider()', () => { }; }); - ruleDataService.findIndicesByFeature.mockImplementation(() => { - return [ - { - baseName: 'myTestIndex', - } as unknown as IndexInfo, - ]; + ruleDataService.findIndexByFeature.mockImplementation(() => { + return { + baseName: 'myTestIndex', + } as unknown as IndexInfo; }); let searchRequest: RuleRegistrySearchRequest = {} as unknown as RuleRegistrySearchRequest; @@ -199,8 +195,8 @@ describe('ruleRegistrySearchStrategyProvider()', () => { request: {}, }; - ruleDataService.findIndicesByFeature.mockImplementationOnce(() => { - return []; + ruleDataService.findIndexByFeature.mockImplementationOnce(() => { + return null; }); const strategy = ruleRegistrySearchStrategyProvider( diff --git a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts index 851ffbdc098da..255af29a9a9d3 100644 --- a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts +++ b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts @@ -89,17 +89,16 @@ export const ruleRegistrySearchStrategyProvider = ( ); return accum; } - - return [ - ...accum, - ...ruleDataService - .findIndicesByFeature(featureId, Dataset.alerts) - .map((indexInfo) => { - return featureId === 'siem' - ? `${indexInfo.baseName}-${space?.id ?? ''}*` - : `${indexInfo.baseName}*`; - }), - ]; + const alertIndexInfo = ruleDataService.findIndexByFeature(featureId, Dataset.alerts); + if (alertIndexInfo) { + return [ + ...accum, + featureId === 'siem' + ? `${alertIndexInfo.baseName}-${space?.id ?? ''}*` + : `${alertIndexInfo.baseName}*`, + ]; + } + return accum; }, []); if (indices.length === 0) { diff --git a/x-pack/plugins/screenshotting/common/index.ts b/x-pack/plugins/screenshotting/common/index.ts index b6b9034cb8189..7570477a1c1c9 100644 --- a/x-pack/plugins/screenshotting/common/index.ts +++ b/x-pack/plugins/screenshotting/common/index.ts @@ -14,3 +14,5 @@ export { SCREENSHOTTING_EXPRESSION, SCREENSHOTTING_EXPRESSION_INPUT, } from './expression'; + +export const PLUGIN_ID = 'screenshotting'; diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts index 7a2453b2a426b..ce28c53bb5f88 100644 --- a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts +++ b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { groupBy } from 'lodash'; import type { Values } from '@kbn/utility-types'; -import type { Logger, PackageInfo } from '@kbn/core/server'; +import { groupBy } from 'lodash'; +import type { PackageInfo } from '@kbn/core/server'; import type { LayoutParams } from '../../../common'; import { LayoutTypes } from '../../../common'; import type { Layout } from '../../layouts'; -import type { CaptureOptions, CaptureResult, CaptureMetrics } from '../../screenshots'; +import type { CaptureMetrics, CaptureOptions, CaptureResult } from '../../screenshots'; +import { EventLogger, Transactions } from '../../screenshots/event_logger'; import { pngsToPdf } from './pdf_maker'; /** @@ -92,7 +93,7 @@ function getTimeRange(results: CaptureResult['results']) { } export async function toPdf( - logger: Logger, + eventLogger: EventLogger, packageInfo: PackageInfo, layout: Layout, { logo, title }: PdfScreenshotOptions, @@ -106,7 +107,7 @@ export async function toPdf( layout, logo, packageInfo, - logger, + eventLogger, }); return { @@ -119,7 +120,8 @@ export async function toPdf( renderErrors: results.flatMap(({ renderErrors }) => renderErrors ?? []), }; } catch (error) { - logger.error(`Could not generate the PDF buffer!`); + eventLogger.kbnLogger.error(`Could not generate the PDF buffer!`); + eventLogger.error(error, Transactions.PDF); throw error; } diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts index be69ec4c5e141..280b9173c7920 100644 --- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts +++ b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts @@ -5,17 +5,17 @@ * 2.0. */ -import type { Logger, PackageInfo } from '@kbn/core/server'; -import { PdfMaker } from './pdfmaker'; +import type { PackageInfo } from '@kbn/core/server'; import type { Layout } from '../../../layouts'; -import { getTracker } from './tracker'; import type { CaptureResult } from '../../../screenshots'; +import { Actions, EventLogger, Transactions } from '../../../screenshots/event_logger'; +import { PdfMaker } from './pdfmaker'; interface PngsToPdfArgs { results: CaptureResult['results']; layout: Layout; packageInfo: PackageInfo; - logger: Logger; + eventLogger: EventLogger; logo?: string; title?: string; } @@ -26,37 +26,43 @@ export async function pngsToPdf({ logo, title, packageInfo, - logger, + eventLogger, }: PngsToPdfArgs): Promise<{ buffer: Buffer; pages: number }> { - const pdfMaker = new PdfMaker(layout, logo, packageInfo, logger); - const tracker = getTracker(); - if (title) { - pdfMaker.setTitle(title); - } - results.forEach((result) => { - result.screenshots.forEach((png) => { - tracker.startAddImage(); - pdfMaker.addImage(png.data, { - title: png.title ?? undefined, - description: png.description ?? undefined, - }); - tracker.endAddImage(); - }); - }); + const { kbnLogger } = eventLogger; + const transactionEnd = eventLogger.startTransaction(Transactions.PDF); let buffer: Uint8Array | null = null; + let pdfMaker: PdfMaker | null = null; try { - tracker.startCompile(); + pdfMaker = new PdfMaker(layout, logo, packageInfo, kbnLogger); + if (title) { + pdfMaker.setTitle(title); + } + results.forEach((result) => { + result.screenshots.forEach((png) => { + const spanEnd = eventLogger.logPdfEvent( + 'add image to PDF file', + Actions.ADD_IMAGE, + 'output' + ); + pdfMaker?.addImage(png.data, { + title: png.title ?? undefined, + description: png.description ?? undefined, + }); + spanEnd(); + }); + }); + + const spanEnd = eventLogger.logPdfEvent('compile PDF file', Actions.COMPILE, 'output'); buffer = await pdfMaker.generate(); - tracker.endCompile(); + spanEnd(); const byteLength = buffer?.byteLength ?? 0; - logger.debug(`PDF buffer byte length: ${byteLength}`); - tracker.setByteLength(byteLength); - } catch (err) { - throw err; - } finally { - tracker.end(); + transactionEnd({ labels: { byte_length_pdf: byteLength, pdf_pages: pdfMaker.getPageCount() } }); + } catch (error) { + kbnLogger.error(error); + eventLogger.error(error, Actions.COMPILE); + throw error; } return { buffer: Buffer.from(buffer.buffer), pages: pdfMaker.getPageCount() }; diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/tracker.ts b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/tracker.ts deleted file mode 100644 index 49576a03d18a3..0000000000000 --- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/tracker.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 apm from 'elastic-apm-node'; - -interface PdfTracker { - setByteLength: (byteLength: number) => void; - startAddImage: () => void; - endAddImage: () => void; - startCompile: () => void; - endCompile: () => void; - end: () => void; -} - -const TRANSACTION_TYPE = 'reporting'; // TODO: Find out whether we can rename to "screenshotting"; -const SPANTYPE_OUTPUT = 'output'; - -interface ApmSpan { - end: () => void; -} - -export function getTracker(): PdfTracker { - const apmTrans = apm.startTransaction('generate-pdf', TRANSACTION_TYPE); - - let apmAddImage: ApmSpan | null = null; - let apmCompilePdf: ApmSpan | null = null; - - return { - startAddImage() { - apmAddImage = apmTrans?.startSpan('add-pdf-image', SPANTYPE_OUTPUT) || null; - }, - endAddImage() { - apmAddImage?.end(); - }, - startCompile() { - apmCompilePdf = apmTrans?.startSpan('compile-pdf', SPANTYPE_OUTPUT) || null; - }, - endCompile() { - apmCompilePdf?.end(); - }, - setByteLength(byteLength: number) { - apmTrans?.setLabel('byte-length', byteLength, false); - }, - end() { - apmTrans?.end(); - }, - }; -} diff --git a/x-pack/plugins/screenshotting/server/plugin.ts b/x-pack/plugins/screenshotting/server/plugin.ts index 27da8b3430e6d..144b88a2c1c75 100755 --- a/x-pack/plugins/screenshotting/server/plugin.ts +++ b/x-pack/plugins/screenshotting/server/plugin.ts @@ -85,11 +85,9 @@ export class ScreenshottingPlugin implements Plugin { const browserDriverFactory = await this.browserDriverFactory; - const logger = this.logger.get('screenshot'); - return new Screenshots( browserDriverFactory, - logger, + this.logger, this.packageInfo, http, this.config, diff --git a/x-pack/plugins/screenshotting/server/screenshots/__snapshots__/index.test.ts.snap b/x-pack/plugins/screenshotting/server/screenshots/__snapshots__/index.test.ts.snap index c0971c9b95763..1b3826ce9980d 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/__snapshots__/index.test.ts.snap +++ b/x-pack/plugins/screenshotting/server/screenshots/__snapshots__/index.test.ts.snap @@ -20,7 +20,7 @@ Array [ }, }, ], - "error": [Error: The "wait for elements" phase encountered an error: Error: An error occurred when trying to read the page for visualization panel info: Mock error!], + "error": [Error: The "wait for elements" phase encountered an error: Error: Mock error!], "renderErrors": undefined, "screenshots": Array [ Object { @@ -63,7 +63,7 @@ Array [ }, }, ], - "error": [Error: The "wait for elements" phase encountered an error: Error: An error occurred when trying to read the page for visualization panel info: Mock error!], + "error": [Error: The "wait for elements" phase encountered an error: Error: Mock error!], "renderErrors": undefined, "screenshots": Array [ Object { diff --git a/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.test.ts b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.test.ts new file mode 100644 index 0000000000000..3a20c404ff497 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.test.ts @@ -0,0 +1,261 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { Actions, EventLogger, ScreenshottingAction, Transactions } from '.'; +import { ElementPosition } from '../get_element_position_data'; +import { ConfigType } from '../../config'; + +jest.mock('uuid', () => ({ + v4: () => 'NEW_UUID', +})); + +type EventLoggerArgs = [message: string, meta: ScreenshottingAction]; +describe('Event Logger', () => { + let eventLogger: EventLogger; + let config: ConfigType; + let logSpy: jest.SpyInstance; + + beforeEach(() => { + const testDate = moment(new Date('2021-04-12T16:00:00.000Z')); + let delaySeconds = 1; + + jest.spyOn(global.Date, 'now').mockImplementation(() => { + return testDate.add(delaySeconds++, 'seconds').valueOf(); + }); + + const logger = loggingSystemMock.createLogger(); + config = { capture: { zoom: 2 } } as ConfigType; + eventLogger = new EventLogger(logger, config); + + logSpy = jest.spyOn(logger, 'debug') as jest.SpyInstance; + }); + + it('creates logs for the events and includes durations and event payload data', () => { + const screenshottingEnd = eventLogger.startTransaction(Transactions.SCREENSHOTTING); + const openUrlEnd = eventLogger.logScreenshottingEvent( + 'open the url to the Kibana application', + Actions.OPEN_URL, + 'wait' + ); + openUrlEnd(); + const getElementPositionsEnd = eventLogger.logScreenshottingEvent( + 'scan the page to find the boundaries of visualization elements', + Actions.GET_ELEMENT_POSITION_DATA, + 'wait' + ); + getElementPositionsEnd(); + screenshottingEnd({ + labels: { + cpu: 12, + cpu_percentage: 0, + memory: 450789, + memory_mb: 449, + byte_length: 14000, + }, + }); + + const pdfEnd = eventLogger.startTransaction(Transactions.PDF); + const addImageEnd = eventLogger.logPdfEvent( + 'add image to the PDF file', + Actions.ADD_IMAGE, + 'output' + ); + addImageEnd(); + pdfEnd({ labels: { pdf_pages: 1, byte_length_pdf: 6666 } }); + + const logs = logSpy.mock.calls.map(([message, data]) => ({ + message, + duration: data?.event?.duration, + screenshotting: data?.kibana?.screenshotting, + })); + + expect(logs.length).toBe(10); + expect(logs).toMatchInlineSnapshot(` + Array [ + Object { + "duration": undefined, + "message": "starting: screenshot-pipeline", + "screenshotting": Object { + "action": "screenshot-pipeline-start", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": undefined, + "message": "starting: open the url to the Kibana application", + "screenshotting": Object { + "action": "open-url-start", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": 3000, + "message": "completed: open the url to the Kibana application", + "screenshotting": Object { + "action": "open-url-complete", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": undefined, + "message": "starting: scan the page to find the boundaries of visualization elements", + "screenshotting": Object { + "action": "get-element-position-data-start", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": 5000, + "message": "completed: scan the page to find the boundaries of visualization elements", + "screenshotting": Object { + "action": "get-element-position-data-complete", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": 20000, + "message": "completed: screenshot-pipeline", + "screenshotting": Object { + "action": "screenshot-pipeline-complete", + "byte_length": 14000, + "cpu": 12, + "cpu_percentage": 0, + "memory": 450789, + "memory_mb": 449, + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": undefined, + "message": "starting: generate-pdf", + "screenshotting": Object { + "action": "generate-pdf-start", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": undefined, + "message": "starting: add image to the PDF file", + "screenshotting": Object { + "action": "add-pdf-image-start", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": 9000, + "message": "completed: add image to the PDF file", + "screenshotting": Object { + "action": "add-pdf-image-complete", + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": 27000, + "message": "completed: generate-pdf", + "screenshotting": Object { + "action": "generate-pdf-complete", + "byte_length_pdf": 6666, + "pdf_pages": 1, + "session_id": "NEW_UUID", + }, + }, + ] + `); + }); + + it('logs the number of pixels', () => { + const elementPosition = { + boundingClientRect: { width: 1350, height: 2000 }, + scroll: {}, + } as ElementPosition; + const endScreenshot = eventLogger.logScreenshottingEvent( + 'screenshot capture test', + Actions.GET_SCREENSHOT, + 'read', + eventLogger.getPixelsFromElementPosition(elementPosition) + ); + endScreenshot({ byte_length: 4444 }); + + const logData = logSpy.mock.calls.map(([message, data]) => ({ + message, + duration: data.event?.duration, + screenshotting: data.kibana.screenshotting, + })); + + expect(logData).toMatchInlineSnapshot(` + Array [ + Object { + "duration": undefined, + "message": "starting: screenshot capture test", + "screenshotting": Object { + "action": "get-screenshots-start", + "pixels": 10800000, + "session_id": "NEW_UUID", + }, + }, + Object { + "duration": 2000, + "message": "completed: screenshot capture test", + "screenshotting": Object { + "action": "get-screenshots-complete", + "byte_length": 4444, + "pixels": 10800000, + "session_id": "NEW_UUID", + }, + }, + ] + `); + }); + + it('creates helpful error logs', () => { + eventLogger.startTransaction(Transactions.SCREENSHOTTING); + eventLogger.logScreenshottingEvent('opening the url', Actions.OPEN_URL, 'wait'); + eventLogger.error(new Error('Something erroneous happened'), Transactions.SCREENSHOTTING); + + const logData = logSpy.mock.calls.map(([message, data]) => ({ + message, + error: data.error, + screenshotting: data.kibana.screenshotting, + })); + + expect(logData).toMatchInlineSnapshot(` + Array [ + Object { + "error": undefined, + "message": "starting: screenshot-pipeline", + "screenshotting": Object { + "action": "screenshot-pipeline-start", + "session_id": "NEW_UUID", + }, + }, + Object { + "error": undefined, + "message": "starting: opening the url", + "screenshotting": Object { + "action": "open-url-start", + "session_id": "NEW_UUID", + }, + }, + Object { + "error": Object { + "code": undefined, + "message": "Something erroneous happened", + "stack_trace": undefined, + "type": undefined, + }, + "message": "Error: Something erroneous happened", + "screenshotting": Object { + "action": "screenshot-pipeline-error", + "session_id": "NEW_UUID", + }, + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts new file mode 100644 index 0000000000000..033fb24c80685 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts @@ -0,0 +1,315 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, LogMeta } from '@kbn/core/server'; +import apm from 'elastic-apm-node'; +import uuid from 'uuid'; +import { CaptureResult } from '..'; +import { PLUGIN_ID } from '../../../common'; +import { ConfigType } from '../../config'; +import { ElementPosition } from '../get_element_position_data'; +import { Screenshot } from '../get_screenshots'; + +export enum Actions { + OPEN_URL = 'open-url', + GET_ELEMENT_POSITION_DATA = 'get-element-position-data', + GET_NUMBER_OF_ITEMS = 'get-number-of-items', + GET_RENDER_ERRORS = 'get-render-errors', + GET_TIMERANGE = 'get-timerange', + INJECT_CSS = 'inject-css', + REPOSITION = 'position-elements', + WAIT_RENDER = 'wait-for-render', + WAIT_VISUALIZATIONS = 'wait-for-visualizations', + GET_SCREENSHOT = 'get-screenshots', + ADD_IMAGE = 'add-pdf-image', + COMPILE = 'compile-pdf', +} + +export enum Transactions { + SCREENSHOTTING = 'screenshot-pipeline', + PDF = 'generate-pdf', +} + +export type SpanTypes = 'setup' | 'read' | 'wait' | 'correction' | 'output'; + +export interface ScreenshottingAction extends LogMeta { + event?: { + duration?: number; // number of nanoseconds from begin to end of an event + provider: typeof PLUGIN_ID; + }; + + message: string; + kibana: { + screenshotting: { + action: Actions | Transactions; + session_id: string; + + // chromium stats + cpu?: number; + cpu_percentage?: number; + memory?: number; + memory_mb?: number; + + // screenshotting stats + items_count?: number; + pixels?: number; + byte_length?: number; + element_positions?: number; + render_errors?: number; + + // pdf stats + byte_length_pdf?: number; + pdf_pages?: number; + }; + }; +} + +interface ErrorAction { + message: string; + code?: string; + stack_trace?: string; + type?: string; +} + +type SimpleEvent = Omit; + +type LogAdapter = ( + message: string, + suffix: 'start' | 'complete' | 'error', + event: Partial, + startTime?: Date | undefined +) => void; + +type Labels = Record; +type TransactionEndFn = (args: { labels: Partial }) => void; +type LogEndFn = (metricData?: Partial) => void; + +function fillLogData( + message: string, + event: Partial, + suffix: 'start' | 'complete' | 'error', + sessionId: string, + duration: number | undefined +) { + let newMessage = message; + if (suffix !== 'error') { + newMessage = `${suffix === 'start' ? 'starting' : 'completed'}: ${message}`; + } + + let interpretedAction: string; + if (suffix === 'error') { + interpretedAction = event.action + '-error'; + } else { + interpretedAction = event.action + `-${suffix}`; + } + + const logData: ScreenshottingAction = { + message: newMessage, + kibana: { + screenshotting: { + ...event, + action: interpretedAction as Actions, + session_id: sessionId, + }, + }, + event: { duration, provider: PLUGIN_ID }, + }; + return logData; +} + +function logAdapter(logger: Logger, sessionId: string) { + const log: LogAdapter = (message, suffix, event, startTime) => { + let duration: number | undefined; + if (startTime != null) { + const start = startTime.valueOf(); + duration = new Date(Date.now()).valueOf() - start.valueOf(); + } + + const logData = fillLogData(message, event, suffix, sessionId, duration); + logger.debug(logData.message, logData); + }; + return log; +} + +/** + * A class to use internal state properties to log timing between actions in the screenshotting pipeline + */ +export class EventLogger { + private spans = new Map(); + private transactions: Record = { + 'screenshot-pipeline': null, + 'generate-pdf': null, + }; + + private sessionId: string; // identifier to track all logs from one screenshotting flow + private logEvent: LogAdapter; + private timings: Partial> = {}; + + constructor(private readonly logger: Logger, private readonly config: ConfigType) { + this.sessionId = uuid.v4(); + this.logEvent = logAdapter(logger.get('events'), this.sessionId); + } + + private startTiming(a: Actions | Transactions) { + this.timings[a] = new Date(Date.now()); + } + + /** + * @returns Logger - original logger + */ + public get kbnLogger() { + return this.logger; + } + + /** + * General method for logging the beginning of any of this plugin's pipeline + * + * @returns {ScreenshottingEndFn} + */ + public startTransaction( + action: Transactions.SCREENSHOTTING | Transactions.PDF + ): TransactionEndFn { + this.transactions[action] = apm.startTransaction(action, PLUGIN_ID); + const transaction = this.transactions[action]; + + this.startTiming(action); + this.logEvent(action, 'start', { action }); + + return ({ labels }) => { + Object.entries(labels).forEach(([label]) => { + const labelField = label as keyof SimpleEvent; + const labelValue = labels[labelField]; + transaction?.setLabel(label, labelValue, false); + }); + + transaction?.end(); + + this.logEvent(action, 'complete', { ...labels, action }, this.timings[action]); + }; + } + + /** + * General event logging function + * + * @param {string} message + * @param {Actions} action - action type for kibana.screenshotting.action + * @param {TransactionType} transaction - name of the internal APM transaction in which to associate the span + * @param {SpanTypes} type - identifier of the span type + * @param {metricsPre} type - optional metrics to add to the "start" log of the event + * @returns {LogEndFn} - function to log the end of the span + */ + public log( + message: string, + action: Actions, + type: SpanTypes, + metricsPre: Partial = {}, + transaction: Transactions + ): LogEndFn { + const txn = this.transactions[transaction]; + const span = txn?.startSpan(action, type); + + this.spans.set(action, span); + this.startTiming(action); + this.logEvent(message, 'start', { ...metricsPre, action }); + + return (metricData = {}) => { + span?.end(); + this.logEvent( + message, + 'complete', + { ...metricsPre, ...metricData, action }, + this.timings[action] + ); + }; + } + + /** + * Logging helper for screenshotting events + */ + public logScreenshottingEvent( + message: string, + action: Actions, + type: SpanTypes, + metricsPre: Partial = {} + ) { + return this.log(message, action, type, metricsPre, Transactions.SCREENSHOTTING); + } + + /** + * Logging helper for screenshotting events + */ + public logPdfEvent( + message: string, + action: Actions, + type: SpanTypes, + metricsPre: Partial = {} + ) { + return this.log(message, action, type, metricsPre, Transactions.PDF); + } + + /** + * Helper function to calculate the byte length of a set of captured PNG images + */ + public getByteLengthFromCaptureResults( + results: CaptureResult['results'] + ): Pick { + const totalByteLength = results.reduce( + (totals, { screenshots }) => + totals + + screenshots.reduce( + (byteLength: number, screenshot: Screenshot) => byteLength + screenshot.data.byteLength, + 0 + ), + 0 + ); + return { byte_length: totalByteLength }; + } + + /** + * Helper function to create the "metricPre" data needed to log the start + * of a screenshot capture event. + */ + public getPixelsFromElementPosition( + elementPosition: ElementPosition + ): Pick { + const { width, height } = elementPosition.boundingClientRect; + const zoom = this.config.capture.zoom; + const pixels = width * zoom * (height * zoom); + return { pixels }; + } + + /** + * General error logger + * + * @param {ErrorAction} error: The error object that was caught + * @param {Actions} action: The screenshotting action type + * @returns void + */ + public error(error: ErrorAction | string, action: Actions | Transactions) { + const isError = typeof error === 'object'; + const message = `Error: ${isError ? error.message : error}`; + + const errorData = { + ...fillLogData( + message, + { action }, + 'error', + this.sessionId, + undefined // + ), + error: { + message: isError ? error.message : error, + code: isError ? error.code : undefined, + stack_trace: isError ? error.stack_trace : undefined, + type: isError ? error.type : undefined, + }, + }; + + this.logger.get('events').debug(message, errorData); + apm.captureError(error as Error | string); + } +} diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts index 915b036acf22e..f3a76ca79d85f 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts @@ -5,20 +5,24 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { createMockBrowserDriver } from '../browsers/mock'; +import { ConfigType } from '../config'; import { createMockLayout } from '../layouts/mock'; +import { EventLogger } from './event_logger'; import { getElementPositionAndAttributes } from './get_element_position_data'; describe('getElementPositionAndAttributes', () => { - const logger = {} as jest.Mocked; let browser: ReturnType; let layout: ReturnType; + let eventLogger: EventLogger; + let config = {} as ConfigType; beforeEach(async () => { browser = createMockBrowserDriver(); layout = createMockLayout(); - + config = { capture: { zoom: 2 } } as ConfigType; + eventLogger = new EventLogger(loggingSystemMock.createLogger(), config); browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); // @see https://github.com/jsdom/jsdom/issues/653 @@ -59,7 +63,7 @@ describe('getElementPositionAndAttributes', () => { /> `; - await expect(getElementPositionAndAttributes(browser, logger, layout)).resolves + await expect(getElementPositionAndAttributes(browser, eventLogger, layout)).resolves .toMatchInlineSnapshot(` Array [ Object { @@ -103,6 +107,6 @@ describe('getElementPositionAndAttributes', () => { }); it('should return null when there are no elements matching', async () => { - await expect(getElementPositionAndAttributes(browser, logger, layout)).resolves.toBeNull(); + await expect(getElementPositionAndAttributes(browser, eventLogger, layout)).resolves.toBeNull(); }); }); diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts index e3235c6d23253..5018701ce2411 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts @@ -5,11 +5,10 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import { Layout } from '../layouts'; import { CONTEXT_ELEMENTATTRIBUTES } from './constants'; +import { Actions, EventLogger } from './event_logger'; export interface AttributesMap { [key: string]: string | null; @@ -36,10 +35,17 @@ export interface ElementsPositionAndAttribute { export const getElementPositionAndAttributes = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, layout: Layout ): Promise => { - const span = apm.startSpan('get_element_position_data', 'read'); + const { kbnLogger } = eventLogger; + + const spanEnd = eventLogger.logScreenshottingEvent( + 'get element position data', + Actions.GET_ELEMENT_POSITION_DATA, + 'read' + ); + const { screenshot: screenshotSelector } = layout.selectors; // data-shared-items-container let elementsPositionAndAttributes: ElementsPositionAndAttribute[] | null; try { @@ -77,7 +83,7 @@ export const getElementPositionAndAttributes = async ( args: [screenshotSelector, { title: 'data-title', description: 'data-description' }], }, { context: CONTEXT_ELEMENTATTRIBUTES }, - logger + kbnLogger ); if (!elementsPositionAndAttributes?.length) { @@ -86,10 +92,13 @@ export const getElementPositionAndAttributes = async ( ); } } catch (err) { + kbnLogger.error(err); + eventLogger.error(err, Actions.GET_ELEMENT_POSITION_DATA); elementsPositionAndAttributes = null; + // no throw } - span?.end(); + spanEnd({ element_positions: elementsPositionAndAttributes?.length }); return elementsPositionAndAttributes; }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.ts index 34b8291eb03da..a7c4f27065bcf 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.ts @@ -5,22 +5,25 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { createMockBrowserDriver } from '../browsers/mock'; +import { ConfigType } from '../config'; import { createMockLayout } from '../layouts/mock'; +import { EventLogger } from './event_logger'; import { getNumberOfItems } from './get_number_of_items'; describe('getNumberOfItems', () => { const timeout = 10; let browser: ReturnType; let layout: ReturnType; - let logger: jest.Mocked; + let eventLogger: EventLogger; + let config = {} as ConfigType; beforeEach(async () => { browser = createMockBrowserDriver(); layout = createMockLayout(); - logger = { debug: jest.fn() } as unknown as jest.Mocked; - + config = { capture: { zoom: 2 } } as ConfigType; + eventLogger = new EventLogger(loggingSystemMock.createLogger(), config); browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); }); @@ -33,7 +36,7 @@ describe('getNumberOfItems', () => {
`; - await expect(getNumberOfItems(browser, logger, timeout, layout)).resolves.toBe(10); + await expect(getNumberOfItems(browser, eventLogger, timeout, layout)).resolves.toBe(10); }); it('should determine the number of items by selector ', async () => { @@ -43,7 +46,7 @@ describe('getNumberOfItems', () => { `; - await expect(getNumberOfItems(browser, logger, timeout, layout)).resolves.toBe(3); + await expect(getNumberOfItems(browser, eventLogger, timeout, layout)).resolves.toBe(3); }); it('should fall back to the selector when the attribute is empty', async () => { @@ -53,6 +56,6 @@ describe('getNumberOfItems', () => { `; - await expect(getNumberOfItems(browser, logger, timeout, layout)).resolves.toBe(2); + await expect(getNumberOfItems(browser, eventLogger, timeout, layout)).resolves.toBe(2); }); }); diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts index 9dab001e4730d..0e4da2fe5cf6a 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts @@ -5,24 +5,27 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import { Layout } from '../layouts'; import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants'; +import { Actions, EventLogger } from './event_logger'; export const getNumberOfItems = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, timeout: number, layout: Layout ): Promise => { - const span = apm.startSpan('get_number_of_items', 'read'); + const { kbnLogger } = eventLogger; + const spanEnd = eventLogger.logScreenshottingEvent( + 'get the number of visualization items on the page', + Actions.GET_NUMBER_OF_ITEMS, + 'read' + ); + const { renderComplete: renderCompleteSelector, itemsCountAttribute } = layout.selectors; let itemsCount: number; - logger.debug('waiting for elements or items count attribute; or not found to interrupt'); - try { // the dashboard is using the `itemsCountAttribute` attribute to let us // know how many items to expect since gridster incrementally adds panels @@ -31,7 +34,7 @@ export const getNumberOfItems = async ( `${renderCompleteSelector},[${itemsCountAttribute}]`, { timeout }, { context: CONTEXT_READMETADATA }, - logger + kbnLogger ); // returns the value of the `itemsCountAttribute` if it's there, otherwise @@ -52,16 +55,15 @@ export const getNumberOfItems = async ( args: [renderCompleteSelector, itemsCountAttribute], }, { context: CONTEXT_GETNUMBEROFITEMS }, - logger + kbnLogger ); } catch (error) { - logger.error(error); - throw new Error( - `An error occurred when trying to read the page for visualization panel info: ${error.message}` - ); + kbnLogger.error(error); + eventLogger.error(error, Actions.GET_NUMBER_OF_ITEMS); + throw error; } - span?.end(); + spanEnd({ items_count: itemsCount }); return itemsCount; }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts index a475e3c614c15..ece25b37725c8 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts @@ -5,21 +5,24 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { createMockBrowserDriver } from '../browsers/mock'; +import { ConfigType } from '../config'; import { createMockLayout } from '../layouts/mock'; +import { EventLogger } from './event_logger'; import { getRenderErrors } from './get_render_errors'; describe('getRenderErrors', () => { let browser: ReturnType; let layout: ReturnType; - let logger: jest.Mocked; + let eventLogger: EventLogger; + let config = {} as ConfigType; beforeEach(async () => { browser = createMockBrowserDriver(); layout = createMockLayout(); - logger = { debug: jest.fn(), warn: jest.fn() } as unknown as jest.Mocked; - + config = { capture: { zoom: 2 } } as ConfigType; + eventLogger = new EventLogger(loggingSystemMock.createLogger(), config); browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); }); @@ -35,7 +38,7 @@ describe('getRenderErrors', () => {
`; - await expect(getRenderErrors(browser, logger, layout)).resolves.toEqual([ + await expect(getRenderErrors(browser, eventLogger, layout)).resolves.toEqual([ 'a test error', 'a test error', 'a test error', @@ -48,6 +51,6 @@ describe('getRenderErrors', () => { `; - await expect(getRenderErrors(browser, logger, layout)).resolves.toEqual(undefined); + await expect(getRenderErrors(browser, eventLogger, layout)).resolves.toEqual(undefined); }); }); diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts index e8beb18911210..44b92ceddbc8d 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts @@ -5,45 +5,59 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import type { Layout } from '../layouts'; import { CONTEXT_GETRENDERERRORS } from './constants'; +import { Actions, EventLogger } from './event_logger'; export const getRenderErrors = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, layout: Layout ): Promise => { - const span = apm.startSpan('get_render_errors', 'read'); - logger.debug('reading render errors'); - const errorsFound: undefined | string[] = await browser.evaluate( - { - fn: (errorSelector, errorAttribute) => { - const visualizations: Element[] = Array.from(document.querySelectorAll(errorSelector)); - const errors: string[] = []; - - visualizations.forEach((visualization) => { - const errorMessage = visualization.getAttribute(errorAttribute); - if (errorMessage) { - errors.push(errorMessage); - } - }); - - return errors.length ? errors : undefined; - }, - args: [layout.selectors.renderError, layout.selectors.renderErrorAttribute], - }, - { context: CONTEXT_GETRENDERERRORS }, - logger + const { kbnLogger } = eventLogger; + + const spanEnd = eventLogger.logScreenshottingEvent( + 'look for render errors', + Actions.GET_RENDER_ERRORS, + 'read' ); - span?.end(); - if (errorsFound?.length) { - logger.warn( - `Found ${errorsFound.length} error messages. See report object for more information.` + let errorsFound: undefined | string[]; + try { + errorsFound = await browser.evaluate( + { + fn: (errorSelector, errorAttribute) => { + const visualizations: Element[] = Array.from(document.querySelectorAll(errorSelector)); + const errors: string[] = []; + + visualizations.forEach((visualization) => { + const errorMessage = visualization.getAttribute(errorAttribute); + if (errorMessage) { + errors.push(errorMessage); + } + }); + + return errors.length ? errors : undefined; + }, + args: [layout.selectors.renderError, layout.selectors.renderErrorAttribute], + }, + { context: CONTEXT_GETRENDERERRORS }, + kbnLogger ); + + const renderErrors = errorsFound?.length; + if (renderErrors) { + kbnLogger.warn( + `Found ${renderErrors} error messages. See report object for more information.` + ); + } + + spanEnd({ render_errors: renderErrors }); + } catch (error) { + kbnLogger.error(error); + eventLogger.error(error, Actions.GET_RENDER_ERRORS); + throw error; } return errorsFound; diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts index 1f104b9bf2d80..c2342280aea20 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts @@ -5,8 +5,10 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { createMockBrowserDriver } from '../browsers/mock'; +import { ConfigType } from '../config'; +import { EventLogger } from './event_logger'; import { getScreenshots } from './get_screenshots'; describe('getScreenshots', () => { @@ -27,12 +29,13 @@ describe('getScreenshots', () => { }, ]; let browser: ReturnType; - let logger: jest.Mocked; + let eventLogger: EventLogger; + let config = {} as ConfigType; beforeEach(async () => { browser = createMockBrowserDriver(); - logger = { info: jest.fn() } as unknown as jest.Mocked; - + config = { capture: { zoom: 2 } } as ConfigType; + eventLogger = new EventLogger(loggingSystemMock.createLogger(), config); browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); }); @@ -41,7 +44,7 @@ describe('getScreenshots', () => { }); it('should return screenshots', async () => { - await expect(getScreenshots(browser, logger, elementsPositionAndAttributes)).resolves + await expect(getScreenshots(browser, eventLogger, elementsPositionAndAttributes)).resolves .toMatchInlineSnapshot(` Array [ Object { @@ -87,7 +90,7 @@ describe('getScreenshots', () => { }); it('should forward elements positions', async () => { - await getScreenshots(browser, logger, elementsPositionAndAttributes); + await getScreenshots(browser, eventLogger, elementsPositionAndAttributes); expect(browser.screenshot).toHaveBeenCalledTimes(2); expect(browser.screenshot).toHaveBeenNthCalledWith( @@ -104,7 +107,7 @@ describe('getScreenshots', () => { browser.screenshot.mockResolvedValue(Buffer.from('')); await expect( - getScreenshots(browser, logger, elementsPositionAndAttributes) + getScreenshots(browser, eventLogger, elementsPositionAndAttributes) ).rejects.toBeInstanceOf(Error); }); }); diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts index 53829b098ee8c..f157649bbb848 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts @@ -5,9 +5,8 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; +import { Actions, EventLogger } from './event_logger'; import type { ElementsPositionAndAttribute } from './get_element_position_data'; export interface Screenshot { @@ -29,33 +28,45 @@ export interface Screenshot { export const getScreenshots = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, elementsPositionAndAttributes: ElementsPositionAndAttribute[] ): Promise => { - logger.info(`taking screenshots`); + const { kbnLogger } = eventLogger; + kbnLogger.info(`taking screenshots`); const screenshots: Screenshot[] = []; - for (let i = 0; i < elementsPositionAndAttributes.length; i++) { - const span = apm.startSpan('get_screenshots', 'read'); - const item = elementsPositionAndAttributes[i]; + try { + for (let i = 0; i < elementsPositionAndAttributes.length; i++) { + const item = elementsPositionAndAttributes[i]; + const endScreenshot = eventLogger.logScreenshottingEvent( + 'screenshot capture', + Actions.GET_SCREENSHOT, + 'read', + eventLogger.getPixelsFromElementPosition(item.position) + ); - const data = await browser.screenshot(item.position); + const data = await browser.screenshot(item.position); - if (!data?.byteLength) { - throw new Error(`Failure in getScreenshots! Screenshot data is void`); - } + if (!data?.byteLength) { + throw new Error(`Failure in getScreenshots! Screenshot data is void`); + } - screenshots.push({ - data, - title: item.attributes.title, - description: item.attributes.description, - }); + screenshots.push({ + data, + title: item.attributes.title, + description: item.attributes.description, + }); - span?.end(); + endScreenshot({ byte_length: data.byteLength }); + } + } catch (error) { + kbnLogger.error(error); + eventLogger.error(error, Actions.GET_SCREENSHOT); + throw error; } - logger.info(`screenshots taken: ${screenshots.length}`); + kbnLogger.info(`screenshots taken: ${screenshots.length}`); return screenshots; }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts index 8484412f5fd94..a7a7b9295068e 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts @@ -5,21 +5,24 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { createMockBrowserDriver } from '../browsers/mock'; +import { ConfigType } from '../config'; import { createMockLayout } from '../layouts/mock'; +import { EventLogger } from './event_logger'; import { getTimeRange } from './get_time_range'; describe('getTimeRange', () => { let browser: ReturnType; let layout: ReturnType; - let logger: jest.Mocked; + let eventLogger: EventLogger; + let config = {} as ConfigType; beforeEach(async () => { browser = createMockBrowserDriver(); layout = createMockLayout(); - logger = { debug: jest.fn(), info: jest.fn() } as unknown as jest.Mocked; - + config = { capture: { zoom: 2 } } as ConfigType; + eventLogger = new EventLogger(loggingSystemMock.createLogger(), config); browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); }); @@ -28,7 +31,7 @@ describe('getTimeRange', () => { }); it('should return null when there is no duration element', async () => { - await expect(getTimeRange(browser, logger, layout)).resolves.toBeNull(); + await expect(getTimeRange(browser, eventLogger, layout)).resolves.toBeNull(); }); it('should return null when duration attrbute is empty', async () => { @@ -36,7 +39,7 @@ describe('getTimeRange', () => {
`; - await expect(getTimeRange(browser, logger, layout)).resolves.toBeNull(); + await expect(getTimeRange(browser, eventLogger, layout)).resolves.toBeNull(); }); it('should return duration', async () => { @@ -44,6 +47,6 @@ describe('getTimeRange', () => {
`; - await expect(getTimeRange(browser, logger, layout)).resolves.toBe('10'); + await expect(getTimeRange(browser, eventLogger, layout)).resolves.toBe('10'); }); }); diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts index 41d902436d36b..f9272fd27ac95 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts @@ -5,19 +5,21 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import { Layout } from '../layouts'; import { CONTEXT_GETTIMERANGE } from './constants'; +import { Actions, EventLogger } from './event_logger'; export const getTimeRange = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, layout: Layout ): Promise => { - const span = apm.startSpan('get_time_range', 'read'); - logger.debug('getting timeRange'); + const spanEnd = eventLogger.logScreenshottingEvent( + 'looking for time range', + Actions.GET_TIMERANGE, + 'read' + ); const timeRange = await browser.evaluate( { @@ -38,16 +40,14 @@ export const getTimeRange = async ( args: [layout.selectors.timefilterDurationAttribute], }, { context: CONTEXT_GETTIMERANGE }, - logger + eventLogger.kbnLogger ); if (timeRange) { - logger.info(`timeRange: ${timeRange}`); - } else { - logger.debug('no timeRange'); + eventLogger.kbnLogger.info(`timeRange: ${timeRange}`); } - span?.end(); + spanEnd(); return timeRange; }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.ts b/x-pack/plugins/screenshotting/server/screenshots/index.ts index b98270547dbec..33404bb5fadc2 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/index.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/index.ts @@ -8,8 +8,6 @@ import type { HttpServiceSetup, KibanaRequest, Logger, PackageInfo } from '@kbn/core/server'; import type { ExpressionAstExpression } from '@kbn/expressions-plugin/common'; import type { Optional } from '@kbn/utility-types'; -import type { Transaction } from 'elastic-apm-node'; -import apm from 'elastic-apm-node'; import ipaddr from 'ipaddr.js'; import { defaultsDeep, sum } from 'lodash'; import { from, Observable, of, throwError } from 'rxjs'; @@ -46,6 +44,7 @@ import { } from '../formats'; import type { Layout } from '../layouts'; import { createLayout } from '../layouts'; +import { EventLogger, Transactions } from './event_logger'; import type { ScreenshotObservableOptions, ScreenshotObservableResult } from './observable'; import { ScreenshotObservableHandler, UrlOrUrlWithContext } from './observable'; import { Semaphore } from './semaphore'; @@ -110,21 +109,18 @@ export class Screenshots { this.semaphore = new Semaphore(config.poolSize); } - private createLayout(transaction: Transaction | null, options: CaptureOptions): Layout { - const apmCreateLayout = transaction?.startSpan('create-layout', 'setup'); + private createLayout(options: CaptureOptions): Layout { const layout = createLayout(options.layout ?? {}); this.logger.debug(`Layout: width=${layout.width} height=${layout.height}`); - apmCreateLayout?.end(); return layout; } private captureScreenshots( + eventLogger: EventLogger, layout: Layout, - transaction: Transaction | null, options: ScreenshotObservableOptions ): Observable { - const apmCreatePage = transaction?.startSpan('create-page', 'wait'); const { browserTimezone } = options; return this.browserDriverFactory @@ -139,24 +135,22 @@ export class Screenshots { .pipe( this.semaphore.acquire(), mergeMap(({ driver, unexpectedExit$, close }) => { - apmCreatePage?.end(); - unexpectedExit$.subscribe({ error: () => transaction?.end() }); - const screen = new ScreenshotObservableHandler( driver, this.config, - this.logger, + eventLogger, layout, options ); return from(options.urls).pipe( concatMap((url, index) => - screen.setupPage(index, url, transaction).pipe( + screen.setupPage(index, url).pipe( catchError((error) => { screen.checkPageIsOpen(); // this fails the job if the browser has closed this.logger.error(error); + eventLogger.error(error, Transactions.SCREENSHOTTING); return of({ ...DEFAULT_SETUP_RESULT, error }); // allow failover screenshot capture }), takeUntil(unexpectedExit$), @@ -166,16 +160,8 @@ export class Screenshots { take(options.urls.length), toArray(), mergeMap((results) => - // At this point we no longer need the page, close it. - close().pipe( - tap(({ metrics }) => { - if (metrics) { - transaction?.setLabel('cpu', metrics.cpu, false); - transaction?.setLabel('memory', metrics.memory, false); - } - }), - map(({ metrics }) => ({ metrics, results })) - ) + // At this point we no longer need the page, close it and send out the results + close().pipe(map(({ metrics }) => ({ metrics, results }))) ) ); }), @@ -243,15 +229,28 @@ export class Screenshots { if (this.systemHasInsufficientMemory()) { return throwError(() => new errors.InsufficientMemoryAvailableOnCloudError()); } - const transaction = apm.startTransaction('screenshot-pipeline', 'screenshotting'); - const layout = this.createLayout(transaction, options); + + const eventLogger = new EventLogger(this.logger, this.config); + const transactionEnd = eventLogger.startTransaction(Transactions.SCREENSHOTTING); + + const layout = this.createLayout(options); const captureOptions = this.getCaptureOptions(options); - return this.captureScreenshots(layout, transaction, captureOptions).pipe( + return this.captureScreenshots(eventLogger, layout, captureOptions).pipe( + tap(({ results, metrics }) => { + transactionEnd({ + labels: { + cpu: metrics?.cpu, + memory: metrics?.memory, + memory_mb: metrics?.memoryInMegabytes, + ...eventLogger.getByteLengthFromCaptureResults(results), + }, + }); + }), mergeMap((result) => { switch (options.format) { case 'pdf': - return toPdf(this.logger, this.packageInfo, layout, options, result); + return toPdf(eventLogger, this.packageInfo, layout, options, result); default: return toPng(result); } diff --git a/x-pack/plugins/screenshotting/server/screenshots/inject_css.ts b/x-pack/plugins/screenshotting/server/screenshots/inject_css.ts index a77cfa8c9e8e6..41426e893ce58 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/inject_css.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/inject_css.ts @@ -7,26 +7,31 @@ import fs from 'fs'; import { promisify } from 'util'; -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import { Layout } from '../layouts'; import { CONTEXT_INJECTCSS } from './constants'; +import { Actions, EventLogger } from './event_logger'; const fsp = { readFile: promisify(fs.readFile) }; export const injectCustomCss = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, layout: Layout ): Promise => { - const span = apm.startSpan('inject_css', 'correction'); - logger.debug('injecting custom css'); - const filePath = layout.getCssOverridesPath(); if (!filePath) { return; } + + const { kbnLogger } = eventLogger; + + const spanEnd = eventLogger.logScreenshottingEvent( + 'inject CSS into the page', + Actions.INJECT_CSS, + 'correction' + ); + const buffer = await fsp.readFile(filePath); try { await browser.evaluate( @@ -40,14 +45,15 @@ export const injectCustomCss = async ( args: [buffer.toString()], }, { context: CONTEXT_INJECTCSS }, - logger + kbnLogger ); } catch (err) { - logger.error(err); + kbnLogger.error(err); + eventLogger.error(err, Actions.INJECT_CSS); throw new Error( `An error occurred when trying to update Kibana CSS for reporting. ${err.message}` ); } - span?.end(); + spanEnd(); }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts index d3acc96411dc6..b282cd32bbd80 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts @@ -5,36 +5,33 @@ * 2.0. */ +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { interval, of, throwError } from 'rxjs'; import { map } from 'rxjs/operators'; -import type { Logger } from '@kbn/core/server'; import { createMockBrowserDriver } from '../browsers/mock'; import type { ConfigType } from '../config'; import { createMockLayout } from '../layouts/mock'; +import { EventLogger } from './event_logger'; import { ScreenshotObservableHandler, ScreenshotObservableOptions } from './observable'; describe('ScreenshotObservableHandler', () => { let browser: ReturnType; let config: ConfigType; let layout: ReturnType; - let logger: jest.Mocked; + let eventLogger: EventLogger; let options: ScreenshotObservableOptions; beforeEach(async () => { browser = createMockBrowserDriver(); config = { capture: { - timeouts: { - openUrl: 30000, - waitForElements: 30000, - renderComplete: 30000, - }, + timeouts: { openUrl: 30000, waitForElements: 30000, renderComplete: 30000 }, loadDelay: 5000, zoom: 13, }, } as ConfigType; layout = createMockLayout(); - logger = { error: jest.fn() } as unknown as jest.Mocked; + eventLogger = new EventLogger(loggingSystemMock.createLogger(), config); options = { headers: { testHeader: 'testHeadValue' }, urls: [], @@ -46,7 +43,7 @@ describe('ScreenshotObservableHandler', () => { describe('waitUntil', () => { let screenshots: ScreenshotObservableHandler; beforeEach(() => { - screenshots = new ScreenshotObservableHandler(browser, config, logger, layout, options); + screenshots = new ScreenshotObservableHandler(browser, config, eventLogger, layout, options); }); it('catches TimeoutError and references the timeout config in a custom message', async () => { @@ -79,7 +76,7 @@ describe('ScreenshotObservableHandler', () => { describe('checkPageIsOpen', () => { let screenshots: ScreenshotObservableHandler; beforeEach(() => { - screenshots = new ScreenshotObservableHandler(browser, config, logger, layout, options); + screenshots = new ScreenshotObservableHandler(browser, config, eventLogger, layout, options); }); it('throws a decorated Error when page is not open', async () => { diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.ts index b19f3f254b2a2..5048d3f0a3be6 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/observable.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.ts @@ -5,15 +5,15 @@ * 2.0. */ -import type { Transaction } from 'elastic-apm-node'; +import type { Headers } from '@kbn/core/server'; import { defer, forkJoin, Observable, throwError } from 'rxjs'; import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators'; -import type { Headers, Logger } from '@kbn/core/server'; import { errors } from '../../common'; import type { Context, HeadlessChromiumDriver } from '../browsers'; import { DEFAULT_VIEWPORT, getChromiumDisconnectedError } from '../browsers'; -import { durationToNumber as toNumber, ConfigType } from '../config'; +import { ConfigType, durationToNumber as toNumber } from '../config'; import type { Layout } from '../layouts'; +import { Actions, EventLogger } from './event_logger'; import type { ElementsPositionAndAttribute } from './get_element_position_data'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getNumberOfItems } from './get_number_of_items'; @@ -90,7 +90,9 @@ interface PageSetupResults { renderErrors?: string[]; } -const getDefaultElementPosition = (dimensions: { height?: number; width?: number } | null) => { +const getDefaultElementPosition = ( + dimensions: { height?: number; width?: number } | null +): ElementsPositionAndAttribute[] => { const height = dimensions?.height || DEFAULT_VIEWPORT.height; const width = dimensions?.width || DEFAULT_VIEWPORT.width; @@ -118,7 +120,7 @@ export class ScreenshotObservableHandler { constructor( private readonly driver: HeadlessChromiumDriver, private readonly config: ConfigType, - private readonly logger: Logger, + private readonly eventLogger: EventLogger, private readonly layout: Layout, private options: ScreenshotObservableOptions ) {} @@ -154,7 +156,7 @@ export class ScreenshotObservableHandler { return openUrl( this.driver, - this.logger, + this.eventLogger, toNumber(this.config.capture.timeouts.openUrl), index, url, @@ -168,52 +170,70 @@ export class ScreenshotObservableHandler { const driver = this.driver; const waitTimeout = toNumber(this.config.capture.timeouts.waitForElements); - return defer(() => getNumberOfItems(driver, this.logger, waitTimeout, this.layout)).pipe( + return defer(() => getNumberOfItems(driver, this.eventLogger, waitTimeout, this.layout)).pipe( mergeMap(async (itemsCount) => { // set the viewport to the dimensions from the job, to allow elements to flow into the expected layout const viewport = this.layout.getViewport(itemsCount) || getDefaultViewPort(); // Set the viewport allowing time for the browser to handle reflow and redraw // before checking for readiness of visualizations. - await driver.setViewport(viewport, this.logger); - await waitForVisualizations(driver, this.logger, waitTimeout, itemsCount, this.layout); + await driver.setViewport(viewport, this.eventLogger.kbnLogger); + await waitForVisualizations(driver, this.eventLogger, waitTimeout, itemsCount, this.layout); }), this.waitUntil(waitTimeout, 'wait for elements') ); } - private completeRender(apmTrans: Transaction | null) { + private completeRender() { const driver = this.driver; const layout = this.layout; - const logger = this.logger; + const eventLogger = this.eventLogger; return defer(async () => { // Waiting till _after_ elements have rendered before injecting our CSS // allows for them to be displayed properly in many cases - await injectCustomCss(driver, logger, layout); + await injectCustomCss(driver, eventLogger, layout); - const apmPositionElements = apmTrans?.startSpan('position-elements', 'correction'); - // position panel elements for print layout - await layout.positionElements?.(driver, logger); - apmPositionElements?.end(); + const spanEnd = this.eventLogger.logScreenshottingEvent( + 'get positions of visualization elements', + Actions.GET_ELEMENT_POSITION_DATA, + 'read' + ); + try { + // position panel elements for print layout + await layout.positionElements?.(driver, eventLogger.kbnLogger); + spanEnd(); + } catch (error) { + eventLogger.error(error, Actions.GET_ELEMENT_POSITION_DATA); + throw error; + } - await waitForRenderComplete(driver, logger, toNumber(this.config.capture.loadDelay), layout); + await waitForRenderComplete( + driver, + eventLogger, + toNumber(this.config.capture.loadDelay), + layout + ); }).pipe( mergeMap(() => forkJoin({ - timeRange: getTimeRange(driver, logger, layout), - elementsPositionAndAttributes: getElementPositionAndAttributes(driver, logger, layout), - renderErrors: getRenderErrors(driver, logger, layout), + timeRange: getTimeRange(driver, eventLogger, layout), + elementsPositionAndAttributes: getElementPositionAndAttributes( + driver, + eventLogger, + layout + ), + renderErrors: getRenderErrors(driver, eventLogger, layout), }) ), this.waitUntil(toNumber(this.config.capture.timeouts.renderComplete), 'render complete') ); } - public setupPage(index: number, url: UrlOrUrlWithContext, apmTrans: Transaction | null) { + public setupPage(index: number, url: UrlOrUrlWithContext) { return this.openUrl(index, url).pipe( switchMapTo(this.waitForElements()), - switchMapTo(this.completeRender(apmTrans)) + switchMapTo(this.completeRender()) ); } @@ -227,7 +247,7 @@ export class ScreenshotObservableHandler { getDefaultElementPosition(this.layout.getViewport(1)); let screenshots: Screenshot[] = []; try { - screenshots = await getScreenshots(this.driver, this.logger, elements); + screenshots = await getScreenshots(this.driver, this.eventLogger, elements); } catch (e) { throw new errors.FailedToCaptureScreenshot(e.message); } diff --git a/x-pack/plugins/screenshotting/server/screenshots/open_url.ts b/x-pack/plugins/screenshotting/server/screenshots/open_url.ts index c557374ff9876..bdf8c678eb1d2 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/open_url.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/open_url.ts @@ -5,33 +5,39 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Headers, Logger } from '@kbn/core/server'; -import type { HeadlessChromiumDriver } from '../browsers'; -import type { Context } from '../browsers'; +import type { Headers } from '@kbn/core/server'; +import type { Context, HeadlessChromiumDriver } from '../browsers'; import { DEFAULT_PAGELOAD_SELECTOR } from './constants'; +import { Actions, EventLogger } from './event_logger'; export const openUrl = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, timeout: number, index: number, url: string, context: Context, headers: Headers ): Promise => { + const { kbnLogger } = eventLogger; + const spanEnd = eventLogger.logScreenshottingEvent('open url', Actions.OPEN_URL, 'wait'); + // If we're moving to another page in the app, we'll want to wait for the app to tell us // it's loaded the next page. const page = index + 1; const waitForSelector = page > 1 ? `[data-shared-page="${page}"]` : DEFAULT_PAGELOAD_SELECTOR; - const span = apm.startSpan('open_url', 'wait'); try { - await browser.open(url, { context, headers, waitForSelector, timeout }, logger); + await browser.open(url, { context, headers, waitForSelector, timeout }, kbnLogger); } catch (err) { - logger.error(err); - throw new Error(`An error occurred when trying to open the Kibana URL: ${err.message}`); + kbnLogger.error(err); + + const newError = new Error( + `An error occurred when trying to open the Kibana URL: ${err.message}` + ); + eventLogger.error(newError, Actions.OPEN_URL); + throw newError; } - span?.end(); + spanEnd(); }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/semaphore.test.ts b/x-pack/plugins/screenshotting/server/screenshots/semaphore/index.test.ts similarity index 98% rename from x-pack/plugins/screenshotting/server/screenshots/semaphore.test.ts rename to x-pack/plugins/screenshotting/server/screenshots/semaphore/index.test.ts index 6d6dd21347974..0cc40a83723a9 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/semaphore.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/semaphore/index.test.ts @@ -6,7 +6,7 @@ */ import { TestScheduler } from 'rxjs/testing'; -import { Semaphore } from './semaphore'; +import { Semaphore } from '.'; describe('Semaphore', () => { let testScheduler: TestScheduler; diff --git a/x-pack/plugins/screenshotting/server/screenshots/semaphore.ts b/x-pack/plugins/screenshotting/server/screenshots/semaphore/index.ts similarity index 100% rename from x-pack/plugins/screenshotting/server/screenshots/semaphore.ts rename to x-pack/plugins/screenshotting/server/screenshots/semaphore/index.ts diff --git a/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts b/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts index cee23616faeac..8cf8174be152f 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts @@ -5,21 +5,22 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import { Layout } from '../layouts'; import { CONTEXT_WAITFORRENDER } from './constants'; +import { Actions, EventLogger } from './event_logger'; export const waitForRenderComplete = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, loadDelay: number, layout: Layout ) => { - const span = apm.startSpan('wait_for_render', 'wait'); - - logger.debug('waiting for rendering to complete'); + const spanEnd = eventLogger.logScreenshottingEvent( + 'wait for render complete', + Actions.WAIT_RENDER, + 'wait' + ); return await browser .evaluate( @@ -66,11 +67,9 @@ export const waitForRenderComplete = async ( args: [layout.selectors.renderComplete, loadDelay], }, { context: CONTEXT_WAITFORRENDER }, - logger + eventLogger.kbnLogger ) .then(() => { - logger.debug('rendering is complete'); - - span?.end(); + spanEnd(); }); }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts b/x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts index a7485545cdef0..cf49fbe7dc798 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts @@ -5,11 +5,10 @@ * 2.0. */ -import apm from 'elastic-apm-node'; -import type { Logger } from '@kbn/core/server'; import type { HeadlessChromiumDriver } from '../browsers'; import { Layout } from '../layouts'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; +import { Actions, EventLogger } from './event_logger'; interface CompletedItemsCountParameters { context: string; @@ -37,15 +36,21 @@ const getCompletedItemsCount = ({ */ export const waitForVisualizations = async ( browser: HeadlessChromiumDriver, - logger: Logger, + eventLogger: EventLogger, timeout: number, toEqual: number, layout: Layout ): Promise => { - const span = apm.startSpan('wait_for_visualizations', 'wait'); + const { kbnLogger } = eventLogger; + const spanEnd = eventLogger.logScreenshottingEvent( + 'waiting for each visualization to complete rendering', + Actions.WAIT_VISUALIZATIONS, + 'wait' + ); + const { renderComplete: renderCompleteSelector } = layout.selectors; - logger.debug(`waiting for ${toEqual} rendered elements to be in the DOM`); + kbnLogger.debug(`waiting for ${toEqual} rendered elements to be in the DOM`); try { await browser.waitFor({ @@ -54,13 +59,15 @@ export const waitForVisualizations = async ( timeout, }); - logger.debug(`found ${toEqual} rendered elements in the DOM`); + kbnLogger.debug(`found ${toEqual} rendered elements in the DOM`); } catch (err) { - logger.error(err); - throw new Error( + kbnLogger.error(err); + const newError = new Error( `An error occurred when trying to wait for ${toEqual} visualizations to finish rendering. ${err.message}` ); + eventLogger.error(newError, Actions.WAIT_VISUALIZATIONS); + throw newError; } - span?.end(); + spanEnd(); }; diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index 8596f80d09d46..c4da245f48fcd 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -128,6 +128,7 @@ describe(`feature_privilege_builder`, () => { Array [ "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices", ] `); }); @@ -175,6 +176,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices", ] `); }); @@ -263,12 +265,13 @@ describe(`feature_privilege_builder`, () => { }); expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` - Array [ - "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", - "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", - "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", - ] - `); + Array [ + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", + ] + `); }); test('grants `all` privileges to rules and alerts under feature consumer', () => { @@ -326,6 +329,7 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unsnooze", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", ] `); @@ -420,14 +424,16 @@ describe(`feature_privilege_builder`, () => { }); expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` - Array [ - "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", - "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", - "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/get", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/find", - ] - `); + Array [ + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/getAuthorizedAlertsIndices", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/get", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/find", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/getAuthorizedAlertsIndices", + ] + `); }); test('grants both `all` and `read` to rules and alerts privileges under feature consumer', () => { @@ -490,9 +496,11 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/find", "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/find", + "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/getAuthorizedAlertsIndices", "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/update", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/find", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/getAuthorizedAlertsIndices", ] `); }); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index c23a989951f52..3a692e935cf37 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -18,7 +18,7 @@ enum AlertingEntity { const readOperations: Record = { rule: ['get', 'getRuleState', 'getAlertSummary', 'getExecutionLog', 'find'], - alert: ['get', 'find'], + alert: ['get', 'find', 'getAuthorizedAlertsIndices'], }; const writeOperations: Record = { diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index ea2f83a32182e..1d898901455e7 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -121,10 +121,12 @@ export enum SecurityPageName { usersExternalAlerts = 'users-external_alerts', threatHuntingLanding = 'threat-hunting', dashboardsLanding = 'dashboards', + manageLanding = 'manage', } export const THREAT_HUNTING_PATH = '/threat_hunting' as const; export const DASHBOARDS_PATH = '/dashboards' as const; +export const MANAGE_PATH = '/manage' as const; export const TIMELINES_PATH = '/timelines' as const; export const CASES_PATH = '/cases' as const; export const OVERVIEW_PATH = '/overview' as const; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts index f73e60487a4a8..af005d1b60a8f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts @@ -53,8 +53,6 @@ export enum RuleExecutionStatus { export const ruleExecutionStatus = enumeration('RuleExecutionStatus', RuleExecutionStatus); -export type RuleExecutionStatusType = t.TypeOf; - export const ruleExecutionStatusOrder = PositiveInteger; export type RuleExecutionStatusOrder = t.TypeOf; @@ -130,7 +128,7 @@ export const aggregateRuleExecutionEvent = t.type({ timed_out: t.boolean, indexing_duration_ms: t.number, search_duration_ms: t.number, - gap_duration_ms: t.number, + gap_duration_s: t.number, security_status: t.string, security_message: t.string, }); @@ -140,7 +138,7 @@ export type AggregateRuleExecutionEvent = t.TypeOf( 'DefaultStatusFiltersStringArray', t.array(ruleExecutionStatus).is, - (input, context): Either => { + (input, context): Either => { if (input == null) { return t.success([]); } else if (typeof input === 'string') { - return t.array(ruleExecutionStatus).validate(input.split(','), context); + if (input === '') { + return t.success([]); + } else { + return t.array(ruleExecutionStatus).validate(input.split(','), context); + } } else { return t.array(ruleExecutionStatus).validate(input, context); } diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index caeeaa0c17bee..cb03788aa17ba 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -318,7 +318,7 @@ export enum TimelineId { usersPageExternalAlerts = 'users-page-external-alerts', hostsPageEvents = 'hosts-page-events', hostsPageExternalAlerts = 'hosts-page-external-alerts', - hostsPageSessions = 'hosts-page-sessions', + hostsPageSessions = 'hosts-page-sessions-v2', // the v2 is to cache bust localstorage settings as default columns were reworked. detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', diff --git a/x-pack/plugins/security_solution/cypress/README.md b/x-pack/plugins/security_solution/cypress/README.md index 69a8019146ae6..e0430ea332e99 100644 --- a/x-pack/plugins/security_solution/cypress/README.md +++ b/x-pack/plugins/security_solution/cypress/README.md @@ -203,7 +203,7 @@ yarn kbn bootstrap # load auditbeat data needed for test execution (which FTR normally does for us) cd x-pack/plugins/security_solution -node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http(s)://:@ --kibana-url http(s)://:@ +node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http(s)://:@ --kibana-url http(s)://:@ # launch the cypress test runner with overridden environment variables cd x-pack/plugins/security_solution @@ -221,7 +221,7 @@ yarn kbn bootstrap # load auditbeat data needed for test execution (which FTR normally does for us) cd x-pack/plugins/security_solution -node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http(s)://:@ --kibana-url http(s)://:@ +node ../../../scripts/es_archiver load auditbeat --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http(s)://:@ --kibana-url http(s)://:@ # launch the cypress test runner with overridden environment variables cd x-pack/plugins/security_solution @@ -344,13 +344,13 @@ We use es_archiver to manage the data that our Cypress tests need. 3. When you are sure that you have all the data you need run the following command from: `x-pack/plugins/security_solution` ```sh -node ../../../scripts/es_archiver save --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://:@: +node ../../../scripts/es_archiver save --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http://:@: ``` Example: ```sh -node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220 +node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.base.js --es-url http://elastic:changeme@localhost:9220 ``` Note that the command will create the folder if it does not exist. diff --git a/x-pack/plugins/security_solution/cypress/integration/host_details/risk_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/host_details/risk_tab.spec.ts index 3d26173a64c65..089e7584272f8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/host_details/risk_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/host_details/risk_tab.spec.ts @@ -27,7 +27,7 @@ describe('risk tab', () => { cy.get('[data-test-subj="navigation-hostRisk"]').click(); waitForTableToLoad(); - cy.get('[data-test-subj="topHostScoreContributors"]') + cy.get('[data-test-subj="topRiskScoreContributors"]') .find(TABLE_ROWS) .within(() => { cy.get(TABLE_CELL).contains('Unusual Linux Username'); diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts index 9a691c82be7b8..50c06141c7ba9 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts @@ -5,7 +5,7 @@ * 2.0. */ -export const RULE_NAME = '[data-test-subj="topHostScoreContributors"] .euiTableCellContent'; +export const RULE_NAME = '[data-test-subj="topRiskScoreContributors"] .euiTableCellContent'; export const RISK_FLYOUT = '[data-test-subj="open-risk-information-flyout"] .euiFlyoutHeader'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index 9c5780a9a31fc..287281619cb08 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -65,6 +65,7 @@ export const closeAlerts = () => { export const expandFirstAlertActions = () => { cy.get(TIMELINE_CONTEXT_MENU_BTN).should('be.visible'); + cy.get(TIMELINE_CONTEXT_MENU_BTN).find('svg').should('have.class', 'euiIcon-isLoaded'); cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true }); }; @@ -93,8 +94,8 @@ export const setEnrichmentDates = (from?: string, to?: string) => { export const goToClosedAlerts = () => { cy.get(CLOSED_ALERTS_FILTER_BTN).click(); - cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating'); - cy.get(REFRESH_BUTTON).should('have.text', 'Refresh'); + cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); + cy.get(REFRESH_BUTTON).should('have.attr', 'aria-label', 'Refresh query'); cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist'); }; @@ -104,13 +105,13 @@ export const goToManageAlertsDetectionRules = () => { export const goToOpenedAlerts = () => { cy.get(OPENED_ALERTS_FILTER_BTN).click({ force: true }); - cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating'); - cy.get(REFRESH_BUTTON).should('have.text', 'Refresh'); + cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); + cy.get(REFRESH_BUTTON).should('have.attr', 'aria-label', 'Refresh query'); }; export const refreshAlerts = () => { // ensure we've refetched fields the first time index is defined - cy.get(REFRESH_BUTTON).should('have.text', 'Refresh'); + cy.get(REFRESH_BUTTON).should('have.attr', 'aria-label', 'Refresh query'); cy.get(REFRESH_BUTTON).first().click({ force: true }); }; @@ -126,8 +127,8 @@ export const openAlerts = () => { export const goToAcknowledgedAlerts = () => { cy.get(ACKNOWLEDGED_ALERTS_FILTER_BTN).click(); - cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating'); - cy.get(REFRESH_BUTTON).should('have.text', 'Refresh'); + cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); + cy.get(REFRESH_BUTTON).should('have.attr', 'aria-label', 'Refresh query'); cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist'); }; @@ -153,7 +154,7 @@ export const investigateFirstAlertInTimeline = () => { }; export const waitForAlerts = () => { - cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating'); + cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); }; export const waitForAlertsPanelToBeLoaded = () => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts b/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts index 83ec1536baf0f..588a1e94cf407 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/es_archiver.ts @@ -8,7 +8,7 @@ import Path from 'path'; const ES_ARCHIVE_DIR = '../../test/security_solution_cypress/es_archives'; -const CONFIG_PATH = '../../test/functional/config.js'; +const CONFIG_PATH = '../../test/functional/config.base.js'; const ES_URL = Cypress.env('ELASTICSEARCH_URL'); const KIBANA_URL = Cypress.config().baseUrl; const CCS_ES_URL = Cypress.env('CCS_ELASTICSEARCH_URL'); diff --git a/x-pack/plugins/security_solution/cypress/tasks/hosts/authentications.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/authentications.ts index 508e76851f7ff..cf6c6ae467092 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/hosts/authentications.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/hosts/authentications.ts @@ -10,5 +10,5 @@ import { REFRESH_BUTTON } from '../../screens/security_header'; export const waitForAuthenticationsToBeLoaded = () => { cy.get(AUTHENTICATIONS_TABLE).should('exist'); - cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating'); + cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/hosts/uncommon_processes.ts b/x-pack/plugins/security_solution/cypress/tasks/hosts/uncommon_processes.ts index 66f7f0cb9f3b8..67bec8904a849 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/hosts/uncommon_processes.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/hosts/uncommon_processes.ts @@ -10,5 +10,5 @@ import { REFRESH_BUTTON } from '../../screens/security_header'; export const waitForUncommonProcessesToBeLoaded = () => { cy.get(UNCOMMON_PROCESSES_TABLE).should('exist'); - cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating'); + cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/security_header.ts b/x-pack/plugins/security_solution/cypress/tasks/security_header.ts index 57cf72ed85a6e..a50851fa87c77 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/security_header.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/security_header.ts @@ -21,5 +21,7 @@ export const navigateFromHeaderTo = (page: string) => { }; export const refreshPage = () => { - cy.get(REFRESH_BUTTON).click({ force: true }).should('not.have.text', 'Updating'); + cy.get(REFRESH_BUTTON) + .click({ force: true }) + .should('not.have.attr', 'aria-label', 'Needs updating'); }; diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index edfd46d167368..e9735b8c0b903 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -260,7 +260,7 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ { id: SecurityPageName.hostsRisk, title: i18n.translate('xpack.securitySolution.search.hosts.risk', { - defaultMessage: 'Hosts by risk', + defaultMessage: 'Host risk', }), path: `${HOSTS_PATH}/hostRisk`, experimentalKey: 'riskyHostsEnabled', @@ -355,7 +355,7 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ { id: SecurityPageName.usersRisk, title: i18n.translate('xpack.securitySolution.search.users.risk', { - defaultMessage: 'Users by risk', + defaultMessage: 'User risk', }), path: `${USERS_PATH}/userRisk`, experimentalKey: 'riskyUsersEnabled', diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx index 5331b2376fd9f..30b5b3e4d1339 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx @@ -23,11 +23,6 @@ const MyFlexItem = styled(EuiFlexItem)` } `; -const MyExceptionsContainer = styled(EuiFlexGroup)` - height: 600px; - overflow: hidden; -`; - const MyExceptionItemContainer = styled(EuiFlexGroup)` margin: ${({ theme }) => `0 ${theme.eui.euiSize} ${theme.eui.euiSize} 0`}; `; @@ -55,7 +50,7 @@ const ExceptionsViewerItemsComponent: React.FC = ({ onEditExceptionItem, disableActions, }): JSX.Element => ( - + {showEmpty || showNoResults || isInitLoading ? ( = ({ )} - + ); ExceptionsViewerItemsComponent.displayName = 'ExceptionsViewerItemsComponent'; diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap index 51326d54a6161..4dd14f56997eb 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap @@ -7,7 +7,7 @@ exports[`rendering renders correctly 1`] = ` (({ children, show = return ( - + {children} diff --git a/x-pack/plugins/security_solution/public/common/components/formatted_date/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/formatted_date/index.test.tsx index d25294879a572..a981fb2d2b3f2 100644 --- a/x-pack/plugins/security_solution/public/common/components/formatted_date/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/formatted_date/index.test.tsx @@ -43,6 +43,18 @@ describe('formatted_date', () => { expect(wrapper.text()).toEqual('Feb 25, 2019 @ 22:27:05.000'); }); + test.each([ + ['MMMM D, YYYY', 'February 25, 2019'], + ['MM/D/YY', '02/25/19'], + ['d-m-y', '1-27-2019'], + ])('it renders the date in the correct format: %s', (momentDateFormat, expectedResult) => { + const wrapper = mount( + + ); + + expect(wrapper.text()).toEqual(expectedResult); + }); + test('it renders a UTC ISO8601 date string supplied when no date format configuration exists', () => { mockUseDateFormat.mockImplementation(() => ''); const wrapper = mount(); diff --git a/x-pack/plugins/security_solution/public/common/components/formatted_date/index.tsx b/x-pack/plugins/security_solution/public/common/components/formatted_date/index.tsx index a31448d181ea7..a837f0cbfa3ec 100644 --- a/x-pack/plugins/security_solution/public/common/components/formatted_date/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/formatted_date/index.tsx @@ -97,16 +97,18 @@ interface FormattedDateProps { className?: string; fieldName: string; value?: string | number | null; + dateFormat?: string; } export const FormattedDate = React.memo( - ({ value, fieldName, className = '' }): JSX.Element => { + ({ value, fieldName, className = '', dateFormat }): JSX.Element => { if (value == null) { return getOrEmptyTagFromValue(value); } + const maybeDate = getMaybeDate(value); return maybeDate.isValid() ? ( - + ) : ( getOrEmptyTagFromValue(value) diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index fb244c40d6e3d..0dd0bba916cc9 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -38,9 +38,10 @@ import { isUrlInvalid } from '../../utils/validators'; import * as i18n from './translations'; import { SecurityPageName } from '../../../app/types'; -import { getUsersDetailsUrl } from '../link_to/redirect_to_users'; +import { getTabsOnUsersDetailsUrl, getUsersDetailsUrl } from '../link_to/redirect_to_users'; import { LinkAnchor, GenericLinkButton, PortContainer, Comma, LinkButton } from './helpers'; import { HostsTableType } from '../../../hosts/store/model'; +import { UsersTableType } from '../../../users/store/model'; export { LinkButton, LinkAnchor } from './helpers'; @@ -52,10 +53,11 @@ const UserDetailsLinkComponent: React.FC<{ /** `Component` is only used with `EuiDataGrid`; the grid keeps a reference to `Component` for show / hide functionality */ Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon; userName: string; + userTab?: UsersTableType; title?: string; isButton?: boolean; onClick?: (e: SyntheticEvent) => void; -}> = ({ children, Component, userName, isButton, onClick, title }) => { +}> = ({ children, Component, userName, isButton, onClick, title, userTab }) => { const encodedUserName = encodeURIComponent(userName); const { formatUrl, search } = useFormatUrl(SecurityPageName.users); @@ -65,17 +67,29 @@ const UserDetailsLinkComponent: React.FC<{ ev.preventDefault(); navigateToApp(APP_UI_ID, { deepLinkId: SecurityPageName.users, - path: getUsersDetailsUrl(encodedUserName, search), + path: userTab + ? getTabsOnUsersDetailsUrl(encodedUserName, userTab, search) + : getUsersDetailsUrl(encodedUserName, search), }); }, - [encodedUserName, navigateToApp, search] + [encodedUserName, navigateToApp, search, userTab] + ); + + const href = useMemo( + () => + formatUrl( + userTab + ? getTabsOnUsersDetailsUrl(encodedUserName, userTab) + : getUsersDetailsUrl(encodedUserName) + ), + [formatUrl, encodedUserName, userTab] ); return isButton ? ( @@ -85,7 +99,7 @@ const UserDetailsLinkComponent: React.FC<{ {children ? children : userName} diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index 2d38f72b338ee..1620a142c15cb 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -87,6 +87,7 @@ describe('QueryBar ', () => { dataTestSubj: undefined, dateRangeFrom: 'now/d', dateRangeTo: 'now/d', + displayStyle: undefined, filters: [], indexPatterns: [ { @@ -205,6 +206,7 @@ describe('QueryBar ', () => { showQueryBar: true, showQueryInput: true, showSaveQuery: true, + showSubmitButton: false, }); }); @@ -304,7 +306,7 @@ describe('QueryBar ', () => { }); describe('SavedQueryManagementComponent state', () => { - test('popover should hidden when "Save current query" button was clicked', async () => { + test('popover should remain open when "Save current query" button was clicked', async () => { const wrapper = await getWrapper( { /> ); const isSavedQueryPopoverOpen = () => - wrapper.find('EuiPopover[id="savedQueryPopover"]').prop('isOpen'); + wrapper.find('EuiPopover[data-test-subj="queryBarMenuPopover"]').prop('isOpen'); expect(isSavedQueryPopoverOpen()).toBeFalsy(); - wrapper - .find('button[data-test-subj="saved-query-management-popover-button"]') - .simulate('click'); + wrapper.find('button[data-test-subj="showQueryBarMenu"]').simulate('click'); await waitFor(() => { expect(isSavedQueryPopoverOpen()).toBeTruthy(); @@ -338,7 +338,7 @@ describe('QueryBar ', () => { wrapper.find('button[data-test-subj="saved-query-management-save-button"]').simulate('click'); await waitFor(() => { - expect(isSavedQueryPopoverOpen()).toBeFalsy(); + expect(isSavedQueryPopoverOpen()).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx index 15fd5927b7f75..fe8d50d6fab2e 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.tsx @@ -17,7 +17,7 @@ import { SavedQueryTimeFilter, } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; -import { SearchBar } from '@kbn/unified-search-plugin/public'; +import { SearchBar, SearchBarProps } from '@kbn/unified-search-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; export interface QueryBarComponentProps { @@ -36,6 +36,7 @@ export interface QueryBarComponentProps { refreshInterval?: number; savedQuery?: SavedQuery; onSavedQuery: (savedQuery: SavedQuery | undefined) => void; + displayStyle?: SearchBarProps['displayStyle']; } export const QueryBar = memo( @@ -55,6 +56,7 @@ export const QueryBar = memo( savedQuery, onSavedQuery, dataTestSubj, + displayStyle, }) => { const onQuerySubmit = useCallback( (payload: { dateRange: TimeRange; query?: Query }) => { @@ -102,12 +104,11 @@ export const QueryBar = memo( [filterManager] ); - const CustomButton = <>{null}; const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); return ( ( timeHistory={new TimeHistory(new Storage(localStorage))} dataTestSubj={dataTestSubj} savedQuery={savedQuery} + displayStyle={displayStyle} /> ); } diff --git a/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.test.tsx new file mode 100644 index 0000000000000..e07ff93b98c31 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { RiskScoreOverTime, scoreFormatter } from '.'; +import { TestProviders } from '../../mock'; +import { LineSeries } from '@elastic/charts'; + +const mockLineSeries = LineSeries as jest.Mock; + +jest.mock('@elastic/charts', () => { + const original = jest.requireActual('@elastic/charts'); + return { + ...original, + LineSeries: jest.fn().mockImplementation(() => <>), + }; +}); + +describe('Risk Score Over Time', () => { + it('renders', () => { + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('RiskScoreOverTime')).toBeInTheDocument(); + }); + + it('renders loader when loading', () => { + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('RiskScoreOverTime-loading')).toBeInTheDocument(); + }); + + describe('scoreFormatter', () => { + it('renders score formatted', () => { + render( + + + + ); + + const tickFormat = mockLineSeries.mock.calls[0][0].tickFormat; + + expect(tickFormat).toBe(scoreFormatter); + }); + + it('renders a formatted score', () => { + expect(scoreFormatter(3.000001)).toEqual('3'); + expect(scoreFormatter(3.4999)).toEqual('3'); + expect(scoreFormatter(3.51111)).toEqual('4'); + expect(scoreFormatter(3.9999)).toEqual('4'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.tsx b/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.tsx new file mode 100644 index 0000000000000..a7a2dc676abc5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/index.tsx @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, useCallback } from 'react'; +import { + Chart, + LineSeries, + ScaleType, + Settings, + Axis, + Position, + AnnotationDomainType, + LineAnnotation, + TooltipValue, +} from '@elastic/charts'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart, EuiText, EuiPanel } from '@elastic/eui'; +import styled from 'styled-components'; +import { chartDefaultSettings, useTheme } from '../charts/common'; +import { useTimeZone } from '../../lib/kibana'; +import { histogramDateTimeFormatter } from '../utils'; +import { HeaderSection } from '../header_section'; +import { InspectButton, InspectButtonContainer } from '../inspect'; +import * as i18n from './translations'; +import { PreferenceFormattedDate } from '../formatted_date'; +import { RiskScore } from '../../../../common/search_strategy'; + +export interface RiskScoreOverTimeProps { + from: string; + to: string; + loading: boolean; + riskScore?: RiskScore[]; + queryId: string; + title: string; + toggleStatus: boolean; + toggleQuery?: (status: boolean) => void; +} + +const RISKY_THRESHOLD = 70; +const DEFAULT_CHART_HEIGHT = 250; + +const StyledEuiText = styled(EuiText)` + font-size: 9px; + font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold}; + margin-right: ${({ theme }) => theme.eui.paddingSizes.xs}; +`; + +const LoadingChart = styled(EuiLoadingChart)` + display: block; + text-align: center; +`; + +export const scoreFormatter = (d: number) => Math.round(d).toString(); + +const RiskScoreOverTimeComponent: React.FC = ({ + from, + to, + riskScore, + loading, + queryId, + title, + toggleStatus, + toggleQuery, +}) => { + const timeZone = useTimeZone(); + + const dataTimeFormatter = useMemo(() => histogramDateTimeFormatter([from, to]), [from, to]); + const headerFormatter = useCallback( + (tooltip: TooltipValue) => , + [] + ); + + const theme = useTheme(); + + const graphData = useMemo( + () => + riskScore + ?.map((data) => ({ + x: data['@timestamp'], + y: data.risk_stats.risk_score, + })) + .reverse() ?? [], + [riskScore] + ); + + return ( + + + + + + + {toggleStatus && ( + + + + )} + + + {toggleStatus && ( + + +
+ {loading ? ( + + ) : ( + + + + + + + {i18n.RISKY} + + } + /> + + )} +
+
+
+ )} +
+
+ ); +}; + +RiskScoreOverTimeComponent.displayName = 'RiskScoreOverTimeComponent'; +export const RiskScoreOverTime = React.memo(RiskScoreOverTimeComponent); +RiskScoreOverTime.displayName = 'RiskScoreOverTime'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/translations.ts b/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/translations.ts similarity index 75% rename from x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/translations.ts rename to x-pack/plugins/security_solution/public/common/components/risk_score_over_time/translations.ts index 5e1b4ca7410a8..a3d32f5e5d59f 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/risk_score_over_time/translations.ts @@ -7,14 +7,7 @@ import { i18n } from '@kbn/i18n'; -export const HOST_RISK_SCORE_OVER_TIME = i18n.translate( - 'xpack.securitySolution.hosts.hostScoreOverTime.title', - { - defaultMessage: 'Host risk score over time', - } -); - -export const HOST_RISK_THRESHOLD = i18n.translate( +export const RISK_THRESHOLD = i18n.translate( 'xpack.securitySolution.hosts.hostScoreOverTime.riskyThresholdHeader', { defaultMessage: 'Risky threshold', diff --git a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx index d1d5c2fcebc12..d5e9fba36361a 100644 --- a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx @@ -11,7 +11,6 @@ import React, { memo, useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps, useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import { Subscription } from 'rxjs'; -import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; import type { DataViewBase, Filter, Query } from '@kbn/es-query'; @@ -53,12 +52,6 @@ interface SiemSearchBarProps { hideQueryInput?: boolean; } -const SearchBarContainer = styled.div` - .globalQueryBar { - padding: 0px; - } -`; - export const SearchBarComponent = memo( ({ end, @@ -322,7 +315,7 @@ export const SearchBarComponent = memo( const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); return ( - +
( showSaveQuery={true} dataTestSubj={dataTestSubj} /> - +
); }, (prevProps, nextProps) => diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap index 32268e2f21e7f..9d32d2c23b18b 100644 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap @@ -70,34 +70,28 @@ exports[`SessionsView renders correctly against snapshot 1`] = `
- hosts-page-sessions + hosts-page-sessions-v2
- process.start + Started
- process.end + Executable
- process.executable + User
- user.name + Interactive
- process.interactive + Hostname
- process.pid + Type
- host.hostname -
-
- process.entry_leader.entry_meta.type -
-
- process.entry_leader.entry_meta.source.ip + Source IP
diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/cell_renderer.tsx b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/cell_renderer.tsx deleted file mode 100644 index 088935b32ce34..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/cell_renderer.tsx +++ /dev/null @@ -1,25 +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 { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; -import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; -import { getEmptyValue } from '../empty_value'; -import { MAPPED_PROCESS_END_COLUMN } from './default_headers'; - -const hasEcsDataEndEventAction = (ecsData: CellValueElementProps['ecsData']) => { - return ecsData?.event?.action?.includes('end'); -}; - -export const CellRenderer: React.FC = (props: CellValueElementProps) => { - // We only want to render process.end for event.actions of type 'end' - if (props.columnId === MAPPED_PROCESS_END_COLUMN && !hasEcsDataEndEventAction(props.ecsData)) { - return <>{getEmptyValue()}; - } - - return ; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts index d73ab1b690f61..4c045e358e1d6 100644 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts @@ -10,50 +10,52 @@ import { defaultColumnHeaderType } from '../../../timelines/components/timeline/ import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; import { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; - -// Using @timestamp as an way of getting the end time of the process. (Currently endpoint doesn't populate process.end) -// @timestamp of an event.action with value of "end" is what we consider that to be the end time of the process -// Current action are: 'start', 'exec', 'end', so we might have up to three events per process. -export const MAPPED_PROCESS_END_COLUMN = '@timestamp'; +import { + COLUMN_SESSION_START, + COLUMN_EXECUTABLE, + COLUMN_ENTRY_USER, + COLUMN_INTERACTIVE, + COLUMN_HOST_NAME, + COLUMN_ENTRY_TYPE, + COLUMN_ENTRY_IP, +} from './translations'; export const sessionsHeaders: ColumnHeaderOptions[] = [ { columnHeaderType: defaultColumnHeaderType, - id: 'process.start', + id: 'process.entry_leader.start', initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, + display: COLUMN_SESSION_START, }, { columnHeaderType: defaultColumnHeaderType, - id: MAPPED_PROCESS_END_COLUMN, - display: 'process.end', + id: 'process.entry_leader.executable', + display: COLUMN_EXECUTABLE, }, { columnHeaderType: defaultColumnHeaderType, - id: 'process.executable', + id: 'process.entry_leader.user.name', + display: COLUMN_ENTRY_USER, }, { columnHeaderType: defaultColumnHeaderType, - id: 'user.name', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'process.interactive', - }, - { - columnHeaderType: defaultColumnHeaderType, - id: 'process.pid', + id: 'process.entry_leader.interactive', + display: COLUMN_INTERACTIVE, }, { columnHeaderType: defaultColumnHeaderType, id: 'host.hostname', + display: COLUMN_HOST_NAME, }, { columnHeaderType: defaultColumnHeaderType, id: 'process.entry_leader.entry_meta.type', + display: COLUMN_ENTRY_TYPE, }, { - columnHeaderType: defaultColumnHeaderType, id: 'process.entry_leader.entry_meta.source.ip', + columnHeaderType: defaultColumnHeaderType, + display: COLUMN_ENTRY_IP, }, ]; @@ -62,4 +64,11 @@ export const sessionsDefaultModel: SubsetTimelineModel = { columns: sessionsHeaders, defaultColumns: sessionsHeaders, excludedRowRendererIds: Object.values(RowRendererId), + sort: [ + { + columnId: 'process.entry_leader.start', + columnType: 'date', + sortDirection: 'desc', + }, + ], }; diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx index 043a2aa378427..5280f298ba99e 100644 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx @@ -109,10 +109,11 @@ describe('SessionsView', () => { expect(wrapper.getByTestId(`${TEST_PREFIX}:startDate`)).toHaveTextContent(startDate); expect(wrapper.getByTestId(`${TEST_PREFIX}:endDate`)).toHaveTextContent(endDate); expect(wrapper.getByTestId(`${TEST_PREFIX}:timelineId`)).toHaveTextContent( - 'hosts-page-sessions' + 'hosts-page-sessions-v2' ); }); }); + it('passes in the right filters to TGrid', async () => { render( diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx index 6834553a5eee8..4d89b969e5c17 100644 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx @@ -12,7 +12,7 @@ import { ESBoolQuery } from '../../../../common/typed_json'; import { StatefulEventsViewer } from '../events_viewer'; import { sessionsDefaultModel } from './default_headers'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; -import { CellRenderer } from './cell_renderer'; +import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import * as i18n from './translations'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; @@ -24,15 +24,8 @@ export const defaultSessionsFilter: Required> = { bool: { filter: [ { - bool: { - should: [ - { - match: { - 'process.entry_leader.same_as_process': true, - }, - }, - ], - minimum_should_match: 1, + exists: { + field: 'process.entry_leader.entity_id', // to exclude any records which have no entry_leader.entity_id }, }, ], @@ -41,10 +34,10 @@ export const defaultSessionsFilter: Required> = { meta: { alias: null, disabled: false, - key: 'process.entry_leader.same_as_process', + key: 'process.entry_leader.entity_id', negate: false, params: {}, - type: 'boolean', + type: 'string', }, }; @@ -95,7 +88,7 @@ const SessionsViewComponent: React.FC = ({ entityType={entityType} id={timelineId} leadingControlColumns={leadingControlColumns} - renderCellValue={CellRenderer} + renderCellValue={DefaultCellRenderer} rowRenderers={defaultRowRenderers} scopeId={SourcererScopeName.default} start={startDate} diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts index 606ae2b46fc6a..ea35892f3a2f9 100644 --- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts @@ -20,3 +20,52 @@ export const SINGLE_COUNT_OF_SESSIONS = i18n.translate( defaultMessage: 'session', } ); + +export const COLUMN_SESSION_START = i18n.translate( + 'xpack.securitySolution.sessionsView.columnSessionStart', + { + defaultMessage: 'Started', + } +); + +export const COLUMN_EXECUTABLE = i18n.translate( + 'xpack.securitySolution.sessionsView.columnExecutable', + { + defaultMessage: 'Executable', + } +); + +export const COLUMN_ENTRY_USER = i18n.translate( + 'xpack.securitySolution.sessionsView.columnEntryUser', + { + defaultMessage: 'User', + } +); + +export const COLUMN_INTERACTIVE = i18n.translate( + 'xpack.securitySolution.sessionsView.columnInteractive', + { + defaultMessage: 'Interactive', + } +); + +export const COLUMN_HOST_NAME = i18n.translate( + 'xpack.securitySolution.sessionsView.columnHostName', + { + defaultMessage: 'Hostname', + } +); + +export const COLUMN_ENTRY_TYPE = i18n.translate( + 'xpack.securitySolution.sessionsView.columnEntryType', + { + defaultMessage: 'Type', + } +); + +export const COLUMN_ENTRY_IP = i18n.translate( + 'xpack.securitySolution.sessionsView.columnEntrySourceIp', + { + defaultMessage: 'Source IP', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/index.test.tsx new file mode 100644 index 0000000000000..4cc6812772b81 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/index.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { TopRiskScoreContributors } from '.'; +import { TestProviders } from '../../mock'; +import { RuleRisk } from '../../../../common/search_strategy'; + +jest.mock('../../containers/query_toggle'); +jest.mock('../../../risk_score/containers'); + +const testProps = { + riskScore: [], + setQuery: jest.fn(), + deleteQuery: jest.fn(), + hostName: 'test-host-name', + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', + loading: false, + toggleStatus: true, + queryId: 'test-query-id', +}; + +describe('Top Risk Score Contributors', () => { + it('renders', () => { + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('topRiskScoreContributors')).toBeInTheDocument(); + }); + + it('renders sorted items', () => { + const ruleRisk: RuleRisk[] = [ + { + rule_name: 'third', + rule_risk: 10, + rule_id: '3', + }, + { + rule_name: 'first', + rule_risk: 99, + rule_id: '1', + }, + { + rule_name: 'second', + rule_risk: 55, + rule_id: '2', + }, + ]; + + const { queryAllByRole } = render( + + + + ); + + expect(queryAllByRole('row')[1]).toHaveTextContent('first'); + expect(queryAllByRole('row')[2]).toHaveTextContent('second'); + expect(queryAllByRole('row')[3]).toHaveTextContent('third'); + }); + + describe('toggleStatus', () => { + test('toggleStatus=true, render components', () => { + const { queryByTestId } = render( + + + + ); + expect(queryByTestId('topRiskScoreContributors-table')).toBeTruthy(); + }); + + test('toggleStatus=false, do not render components', () => { + const { queryByTestId } = render( + + + + ); + expect(queryByTestId('topRiskScoreContributors-table')).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/index.tsx new file mode 100644 index 0000000000000..7ee2ae5e21413 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/index.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiInMemoryTable, + EuiTableFieldDataColumnType, +} from '@elastic/eui'; + +import { HeaderSection } from '../header_section'; +import { InspectButton, InspectButtonContainer } from '../inspect'; +import * as i18n from './translations'; + +import { RuleRisk } from '../../../../common/search_strategy'; + +import { RuleLink } from '../../../detections/pages/detection_engine/rules/all/use_columns'; + +export interface TopRiskScoreContributorsProps { + loading: boolean; + rules?: RuleRisk[]; + queryId: string; + toggleStatus: boolean; + toggleQuery?: (status: boolean) => void; +} +interface TableItem { + rank: number; + name: string; + id: string; +} + +const columns: Array> = [ + { + name: i18n.RANK_TITLE, + field: 'rank', + width: '45px', + align: 'right', + }, + { + name: i18n.RULE_NAME_TITLE, + field: 'name', + sortable: true, + truncateText: true, + render: (value: TableItem['name'], { id }: TableItem) => + id ? : value, + }, +]; + +const PAGE_SIZE = 5; + +const TopRiskScoreContributorsComponent: React.FC = ({ + rules = [], + loading, + queryId, + toggleStatus, + toggleQuery, +}) => { + const items = useMemo(() => { + return rules + ?.sort((a, b) => b.rule_risk - a.rule_risk) + .map(({ rule_name: name, rule_id: id }, i) => ({ rank: i + 1, name, id })); + }, [rules]); + + const tablePagination = useMemo( + () => ({ + showPerPageOptions: false, + pageSize: PAGE_SIZE, + totalItemCount: items.length, + }), + [items.length] + ); + + return ( + + + + + + + {toggleStatus && ( + + + + )} + + + {toggleStatus && ( + + + + + + )} + + + ); +}; + +export const TopRiskScoreContributors = React.memo(TopRiskScoreContributorsComponent); +TopRiskScoreContributors.displayName = 'TopRiskScoreContributors'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/translations.ts b/x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/translations.ts rename to x-pack/plugins/security_solution/public/common/components/top_risk_score_contributors/translations.ts diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/index.test.tsx index b06fde164af76..162f321b33f6c 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/index.test.tsx @@ -95,6 +95,7 @@ describe('VisualizationActions', () => { addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + remove: jest.fn(), }, }, http: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts index 1bd188d01cb51..ce03d159df5a9 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.test.ts @@ -47,6 +47,7 @@ jest.mock('../../../lib/kibana', () => ({ addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + remove: jest.fn(), }), })); diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx index 1d02d608c0e92..a949478593466 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx @@ -60,6 +60,7 @@ jest.mock('../../lib/kibana', () => ({ addError: jest.fn(), addSuccess: jest.fn(), addWarning: mockAddWarning, + remove: jest.fn(), }), useKibana: () => ({ services: { diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts index c0bb52b20c534..ae3783e82cdbf 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts @@ -11,6 +11,7 @@ const createAppToastsMock = (): jest.Mocked => ({ addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + remove: jest.fn(), api: { get$: jest.fn(), add: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts index 9bea6282e3ad6..359d29be7cd08 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts @@ -30,15 +30,18 @@ describe('useAppToasts', () => { let addErrorMock: jest.Mock; let addSuccessMock: jest.Mock; let addWarningMock: jest.Mock; + let removeMock: jest.Mock; beforeEach(() => { addErrorMock = jest.fn(); addSuccessMock = jest.fn(); addWarningMock = jest.fn(); + removeMock = jest.fn(); (useToasts as jest.Mock).mockImplementation(() => ({ addError: addErrorMock, addSuccess: addSuccessMock, addWarning: addWarningMock, + remove: removeMock, })); }); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts index 4b3b28a54c642..d9c3713f3a4ba 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts @@ -19,7 +19,7 @@ import { IEsError, isEsError } from '@kbn/data-plugin/public'; import { ErrorToastOptions, ToastsStart, Toast } from '@kbn/core/public'; import { useToasts } from '../lib/kibana'; -export type UseAppToasts = Pick & { +export type UseAppToasts = Pick & { api: ToastsStart; addError: (error: unknown, options: ErrorToastOptions) => Toast; }; @@ -36,6 +36,7 @@ export const useAppToasts = (): UseAppToasts => { const addError = useRef(toasts.addError.bind(toasts)).current; const addSuccess = useRef(toasts.addSuccess.bind(toasts)).current; const addWarning = useRef(toasts.addWarning.bind(toasts)).current; + const remove = useRef(toasts.remove.bind(toasts)).current; const _addError = useCallback( (error: unknown, options: ErrorToastOptions) => { @@ -46,8 +47,8 @@ export const useAppToasts = (): UseAppToasts => { ); return useMemo( - () => ({ api: toasts, addError: _addError, addSuccess, addWarning }), - [_addError, addSuccess, addWarning, toasts] + () => ({ api: toasts, addError: _addError, addSuccess, addWarning, remove }), + [_addError, addSuccess, addWarning, remove, toasts] ); }; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts index 0097e31cfee8a..09e7c1fa6e8c8 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -157,6 +157,10 @@ export const createStartServicesMock = ( theme: { theme$: themeServiceMock.createTheme$(), }, + timelines: { + getLastUpdated: jest.fn(), + getFieldBrowser: jest.fn(), + }, } as unknown as StartServices; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx index 3c82161851dd3..539728291156f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx @@ -55,6 +55,7 @@ jest.mock('../../../../common/lib/kibana/kibana_react', () => { addWarning: jest.fn(), addError: jest.fn(), addSuccess: jest.fn(), + remove: jest.fn(), }, }, }, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx index 2ce403a832906..5c8d2643eb445 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx @@ -46,23 +46,7 @@ interface QueryBarDefineRuleProps { const actionTimelineToHide: ActionTimelineToShow[] = ['duplicate', 'createFrom']; -const StyledEuiFormRow = styled(EuiFormRow)` - .kbnTypeahead__items { - max-height: 45vh !important; - } - .globalQueryBar { - padding: 4px 0px 0px 0px; - .kbnQueryBar { - & > div:first-child { - margin: 0px 0px 0px 4px; - } - &__wrap, - &__textarea { - z-index: 0; - } - } - } -`; +const StyledEuiFormRow = styled(EuiFormRow)``; // TODO need to add disabled in the SearchBar @@ -283,6 +267,7 @@ export const QueryBarDefineRule = ({ savedQuery={savedQuery} onSavedQuery={onSavedQuery} hideSavedQuery={false} + displayStyle="inPage" />
)} diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts index d93d667f5fbca..59f79b294d7fa 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts @@ -78,7 +78,7 @@ export const fetchRuleExecutionEvents = async ({ duration_ms: 3866, es_search_duration_ms: 1236, execution_uuid: '88d15095-7937-462c-8f21-9763e1387cad', - gap_duration_ms: 0, + gap_duration_s: 0, indexing_duration_ms: 95, message: "rule executed: siem.queryRule:fb1fc150-a292-11ec-a2cf-c1b28b0392b0: 'Lots of Execution Events'", diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index b999040674a91..c932d0f982bb6 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -630,7 +630,7 @@ describe('Detections Rules API', () => { start: '2001-01-01T17:00:00.000Z', end: '2001-01-02T17:00:00.000Z', queryText: '', - statusFilters: '', + statusFilters: [], signal: abortCtrl.signal, }); @@ -659,7 +659,7 @@ describe('Detections Rules API', () => { start: 'now-30', end: 'now', queryText: '', - statusFilters: '', + statusFilters: [], signal: abortCtrl.signal, }); expect(response).toEqual(responseMock); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index cb78a7a116ae4..220926ebc1722 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { camelCase } from 'lodash'; import dateMath from '@kbn/datemath'; import { HttpStart } from '@kbn/core/public'; @@ -18,7 +19,11 @@ import { DETECTION_ENGINE_RULES_PREVIEW, detectionEngineRuleExecutionEventsUrl, } from '../../../../../common/constants'; -import { BulkAction } from '../../../../../common/detection_engine/schemas/common'; +import { + AggregateRuleExecutionEvent, + BulkAction, + RuleExecutionStatus, +} from '../../../../../common/detection_engine/schemas/common'; import { FullResponseSchema, PreviewResponse, @@ -320,11 +325,11 @@ export const exportRules = async ({ * @param start Start daterange either in UTC ISO8601 or as datemath string (e.g. `2021-12-29T02:44:41.653Z` or `now-30`) * @param end End daterange either in UTC ISO8601 or as datemath string (e.g. `2021-12-29T02:44:41.653Z` or `now/w`) * @param queryText search string in querystring format (e.g. `event.duration > 1000 OR kibana.alert.rule.execution.metrics.execution_gap_duration_s > 100`) - * @param statusFilters comma separated string of `statusFilters` (e.g. `succeeded,failed,partial failure`) + * @param statusFilters RuleExecutionStatus[] array of `statusFilters` (e.g. `succeeded,failed,partial failure`) * @param page current page to fetch * @param perPage number of results to fetch per page - * @param sortField field to sort by - * @param sortOrder what order to sort by (e.g. `asc` or `desc`) + * @param sortField keyof AggregateRuleExecutionEvent field to sort by + * @param sortOrder SortOrder what order to sort by (e.g. `asc` or `desc`) * @param signal AbortSignal Optional signal for cancelling the request * * @throws An error if response is not OK @@ -345,11 +350,11 @@ export const fetchRuleExecutionEvents = async ({ start: string; end: string; queryText?: string; - statusFilters?: string; + statusFilters?: RuleExecutionStatus[]; page?: number; perPage?: number; - sortField?: string; - sortOrder?: string; + sortField?: keyof AggregateRuleExecutionEvent; + sortOrder?: SortOrder; signal?: AbortSignal; }): Promise => { const url = detectionEngineRuleExecutionEventsUrl(ruleId); @@ -361,7 +366,7 @@ export const fetchRuleExecutionEvents = async ({ start: startDate?.utc().toISOString(), end: endDate?.utc().toISOString(), query_text: queryText, - status_filters: statusFilters, + status_filters: statusFilters?.sort()?.join(','), page, per_page: perPage, sort_field: sortField, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts index 533ab6138cb09..8c1737a4519a7 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response'; import { FetchRulesResponse, Rule } from './types'; export const savedRuleMock: Rule = { @@ -126,3 +127,170 @@ export const rulesMock: FetchRulesResponse = { }, ], }; + +export const ruleExecutionEventsMock: GetAggregateRuleExecutionEventsResponse = { + events: [ + { + execution_uuid: 'dc45a63c-4872-4964-a2d0-bddd8b2e634d', + timestamp: '2022-04-28T21:19:08.047Z', + duration_ms: 3, + status: 'failure', + message: 'siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: execution failed', + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 0, + schedule_delay_ms: 2169, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 0, + gap_duration_s: 0, + security_status: 'failed', + security_message: 'Rule failed to execute because rule ran after it was disabled.', + }, + { + execution_uuid: '0fde9271-05d0-4bfb-8ff8-815756d28350', + timestamp: '2022-04-28T21:19:04.973Z', + duration_ms: 1446, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 0, + schedule_delay_ms: 2089, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 2, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, + { + execution_uuid: '5daaa259-ded8-4a52-853e-1e7652d325d5', + timestamp: '2022-04-28T21:19:01.976Z', + duration_ms: 1395, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 1, + schedule_delay_ms: 2637, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 3, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, + { + execution_uuid: 'c7223e1c-4264-4a27-8697-0d720243fafc', + timestamp: '2022-04-28T21:18:58.431Z', + duration_ms: 1815, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 1, + schedule_delay_ms: -255429, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 3, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, + { + execution_uuid: '1f6ba0c1-cc36-4f45-b919-7790b8a8d670', + timestamp: '2022-04-28T21:18:13.954Z', + duration_ms: 2055, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 0, + schedule_delay_ms: 2027, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 0, + gap_duration_s: 0, + security_status: 'partial failure', + security_message: + 'Check privileges failed to execute ResponseError: index_not_found_exception: [index_not_found_exception] Reason: no such index [yup] name: "Click here for hot fresh alerts!" id: "a6e61cf0-c737-11ec-9e32-e14913ffdd2d" rule id: "34946b12-88d1-49ef-82b7-9cad45972030" execution id: "1f6ba0c1-cc36-4f45-b919-7790b8a8d670" space ID: "default"', + }, + { + execution_uuid: 'b0f65d64-b229-432b-9d39-f4385a7f9368', + timestamp: '2022-04-28T21:15:43.086Z', + duration_ms: 1205, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 672, + schedule_delay_ms: 3086, + timed_out: false, + indexing_duration_ms: 140, + search_duration_ms: 684, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, + { + execution_uuid: '7bfd25b9-c0d8-44b1-982c-485169466a8e', + timestamp: '2022-04-28T21:10:40.135Z', + duration_ms: 6321, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 930, + schedule_delay_ms: 1222, + timed_out: false, + indexing_duration_ms: 2103, + search_duration_ms: 946, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, + ], + total: 7, +}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx index 6596cefef4f08..dfeaca617ed24 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx @@ -18,6 +18,7 @@ jest.mock('../../../../common/lib/kibana', () => ({ addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + remove: jest.fn(), }), })); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx index 2a4efb7f69491..5fba7dd7a2ed2 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx @@ -51,7 +51,7 @@ describe('useRuleExecutionEvents', () => { start: 'now-30', end: 'now', queryText: '', - statusFilters: '', + statusFilters: [], }), { wrapper: createReactQueryWrapper(), @@ -92,7 +92,7 @@ describe('useRuleExecutionEvents', () => { duration_ms: 3866, es_search_duration_ms: 1236, execution_uuid: '88d15095-7937-462c-8f21-9763e1387cad', - gap_duration_ms: 0, + gap_duration_s: 0, indexing_duration_ms: 95, message: "rule executed: siem.queryRule:fb1fc150-a292-11ec-a2cf-c1b28b0392b0: 'Lots of Execution Events'", diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx index f2e72858cf392..e18d1f6c2ce5c 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx @@ -5,22 +5,27 @@ * 2.0. */ +import { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { useQuery } from 'react-query'; +import { + AggregateRuleExecutionEvent, + RuleExecutionStatus, +} from '../../../../../common/detection_engine/schemas/common'; import { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { fetchRuleExecutionEvents } from './api'; import * as i18n from './translations'; -interface UseRuleExecutionEventsArgs { +export interface UseRuleExecutionEventsArgs { ruleId: string; start: string; end: string; queryText?: string; - statusFilters?: string; + statusFilters?: RuleExecutionStatus[]; page?: number; perPage?: number; - sortField?: string; - sortOrder?: string; + sortField?: keyof AggregateRuleExecutionEvent; + sortOrder?: SortOrder; } export const useRuleExecutionEvents = ({ @@ -66,6 +71,7 @@ export const useRuleExecutionEvents = ({ }); }, { + keepPreviousData: true, onError: (e) => { addError(e, { title: i18n.RULE_EXECUTION_EVENTS_FETCH_FAILURE }); }, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index a783ac7e1e260..1f066751c2b92 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -98,6 +98,7 @@ jest.mock('../../../common/lib/kibana', () => { addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + remove: jest.fn(), }), }; }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx new file mode 100644 index 0000000000000..257dc8ec512a8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RuleDetailsContextType } from '../rule_details_context'; +import React from 'react'; + +export const useRuleDetailsContextMock = { + create: (): jest.Mocked => ({ + executionLogs: { + state: { + superDatePicker: { + recentlyUsedRanges: [], + refreshInterval: 1000, + isPaused: true, + start: 'now-24h', + end: 'now', + }, + queryText: '', + statusFilters: [], + showMetricColumns: true, + pagination: { + pageIndex: 1, + pageSize: 5, + }, + sort: { + sortField: 'timestamp', + sortDirection: 'desc', + }, + }, + actions: { + setEnd: jest.fn(), + setIsPaused: jest.fn(), + setPageIndex: jest.fn(), + setPageSize: jest.fn(), + setQueryText: jest.fn(), + setRecentlyUsedRanges: jest.fn(), + setRefreshInterval: jest.fn(), + setShowMetricColumns: jest.fn(), + setSortDirection: jest.fn(), + setSortField: jest.fn(), + setStart: jest.fn(), + setStatusFilters: jest.fn(), + }, + }, + }), +}; + +export const useRuleDetailsContext = jest + .fn, []>() + .mockImplementation(useRuleDetailsContextMock.create); + +export const useRuleDetailsContextOptional = jest + .fn, []>() + .mockImplementation(useRuleDetailsContextMock.create); + +export const RulesTableContextProvider = jest + .fn() + .mockImplementation(({ children }: { children: React.ReactNode }) => <>{children}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/execution_log_table.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/execution_log_table.test.tsx.snap deleted file mode 100644 index f93cce70172dc..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/execution_log_table.test.tsx.snap +++ /dev/null @@ -1,140 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ExecutionLogTable snapshots renders correctly against snapshot 1`] = ` - - - - - - - - - - - - - - - Showing 0 rule executions - - - - - - - - - - - , - "render": [Function], - "sortable": false, - "truncateText": false, - "width": "10%", - }, - Object { - "field": "timestamp", - "name": , - "render": [Function], - "sortable": true, - "truncateText": false, - "width": "15%", - }, - Object { - "field": "duration_ms", - "name": , - "render": [Function], - "sortable": true, - "truncateText": false, - "width": "10%", - }, - Object { - "field": "security_message", - "name": , - "render": [Function], - "sortable": false, - "truncateText": false, - "width": "35%", - }, - ] - } - items={Array []} - noItemsMessage={ - - } - onChange={[Function]} - pagination={ - Object { - "pageIndex": 0, - "pageSize": 5, - "pageSizeOptions": Array [ - 5, - 10, - 25, - 50, - ], - "totalItemCount": 0, - } - } - responsive={true} - sorting={ - Object { - "sort": Object { - "direction": "desc", - "field": "timestamp", - }, - } - } - tableLayout="fixed" - /> - -`; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/rule_duration_format.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/rule_duration_format.test.tsx.snap deleted file mode 100644 index ddb59cf52a890..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/rule_duration_format.test.tsx.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RuleDurationFormat snapshots renders correctly against snapshot 1`] = ` - - 00:00:00:000 - -`; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_columns.tsx index c48f5d6971d23..98569b701d93c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_columns.tsx @@ -6,9 +6,9 @@ */ import { EuiBasicTableColumn, EuiHealth, EuiLink, EuiText } from '@elastic/eui'; -import { capitalize } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n-react'; import { DocLinksStart } from '@kbn/core/public'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { capitalize } from 'lodash'; import React from 'react'; import { AggregateRuleExecutionEvent, @@ -19,9 +19,9 @@ import { FormattedDate } from '../../../../../../common/components/formatted_dat import { getStatusColor } from '../../../../../components/rules/rule_execution_status/utils'; import { PopoverTooltip } from '../../all/popover_tooltip'; import { TableHeaderTooltipCell } from '../../all/table_header_tooltip_cell'; +import { RuleDurationFormat } from './rule_duration_format'; import * as i18n from './translations'; -import { RuleDurationFormat } from './rule_duration_format'; export const EXECUTION_LOG_COLUMNS: Array> = [ { @@ -32,7 +32,7 @@ export const EXECUTION_LOG_COLUMNS: Array ), field: 'security_status', - render: (value: RuleExecutionStatus, data) => + render: (value: RuleExecutionStatus) => value ? ( {capitalize(value)} ) : ( @@ -89,7 +89,7 @@ export const GET_EXECUTION_LOG_METRICS_COLUMNS = ( docLinks: DocLinksStart ): Array> => [ { - field: 'gap_duration_ms', + field: 'gap_duration_s', name: ( ), render: (value: number) => ( - <>{value ? : getEmptyValue()} + <>{value ? : getEmptyValue()} ), sortable: true, truncateText: false, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.tsx index b8c2d82ab324e..74804b7c6b557 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.tsx @@ -56,20 +56,23 @@ const statusFilters = statuses.map((status) => ({ interface ExecutionLogTableSearchProps { onlyShowFilters: true; onSearch: (queryText: string) => void; - onStatusFilterChange: (statusFilters: string[]) => void; + onStatusFilterChange: (statusFilters: RuleExecutionStatus[]) => void; + defaultSelectedStatusFilters?: RuleExecutionStatus[]; } /** * SearchBar + StatusFilters component to be used with the Rule Execution Log table - * NOTE: This component is currently not shown in the UI as custom search queries + * NOTE: The SearchBar component is currently not shown in the UI as custom search queries * are not yet fully supported by the Rule Execution Log aggregation API since * certain queries could result in missing data or inclusion of wrong events. * Please see this comment for history/details: https://github.com/elastic/kibana/pull/127339/files#r825240516 */ export const ExecutionLogSearchBar = React.memo( - ({ onlyShowFilters, onSearch, onStatusFilterChange }) => { + ({ onlyShowFilters, onSearch, onStatusFilterChange, defaultSelectedStatusFilters = [] }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [selectedFilters, setSelectedFilters] = useState([]); + const [selectedFilters, setSelectedFilters] = useState( + defaultSelectedStatusFilters + ); const onSearchCallback = useCallback( (queryText: string) => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.test.tsx index 81619d01934ba..608853f004eb9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.test.tsx @@ -5,83 +5,21 @@ * 2.0. */ +import { ruleExecutionEventsMock } from '../../../../../containers/detection_engine/rules/mock'; +import { render, screen } from '@testing-library/react'; +import { TestProviders } from '../../../../../../common/mock'; +import { useRuleDetailsContextMock } from '../__mocks__/rule_details_context'; import React from 'react'; -import { shallow } from 'enzyme'; import { noop } from 'lodash/fp'; +import { useRuleExecutionEvents } from '../../../../../containers/detection_engine/rules'; import { useSourcererDataView } from '../../../../../../common/containers/sourcerer'; +import { useRuleDetailsContext } from '../rule_details_context'; import { ExecutionLogTable } from './execution_log_table'; -jest.mock('../../../../../containers/detection_engine/rules', () => { - const original = jest.requireActual('../../../../../containers/detection_engine/rules'); - return { - ...original, - useRuleExecutionEvents: jest.fn().mockReturnValue({ - loading: true, - setQuery: () => undefined, - data: null, - response: '', - request: '', - refetch: null, - }), - }; -}); - jest.mock('../../../../../../common/containers/sourcerer'); - -jest.mock('../../../../../../common/hooks/use_app_toasts', () => { - const original = jest.requireActual('../../../../../../common/hooks/use_app_toasts'); - - return { - ...original, - useAppToasts: () => ({ - addSuccess: jest.fn(), - addError: jest.fn(), - }), - }; -}); - -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - return { - ...original, - useDispatch: () => jest.fn(), - useSelector: () => jest.fn(), - }; -}); - -jest.mock('../../../../../../common/lib/kibana', () => { - const original = jest.requireActual('../../../../../../common/lib/kibana'); - - return { - ...original, - useUiSetting$: jest.fn().mockReturnValue([]), - useKibana: () => ({ - services: { - data: { - query: { - filterManager: jest.fn().mockReturnValue({}), - }, - }, - docLinks: { - links: { - siem: { - troubleshootGaps: 'link', - }, - }, - }, - storage: { - get: jest.fn(), - set: jest.fn(), - }, - timelines: { - getLastUpdated: jest.fn(), - getFieldBrowser: jest.fn(), - }, - }, - }), - }; -}); +jest.mock('../../../../../containers/detection_engine/rules'); +jest.mock('../rule_details_context'); const mockUseSourcererDataView = useSourcererDataView as jest.Mock; mockUseSourcererDataView.mockReturnValue({ @@ -92,13 +30,20 @@ mockUseSourcererDataView.mockReturnValue({ loading: false, }); -// TODO: Replace snapshot test with base test cases +const mockUseRuleExecutionEvents = useRuleExecutionEvents as jest.Mock; +mockUseRuleExecutionEvents.mockReturnValue({ + data: ruleExecutionEventsMock, + isLoading: false, + isFetching: false, +}); describe('ExecutionLogTable', () => { - describe('snapshots', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + test('Shows total events returned', () => { + const ruleDetailsContext = useRuleDetailsContextMock.create(); + (useRuleDetailsContext as jest.Mock).mockReturnValue(ruleDetailsContext); + render(, { + wrapper: TestProviders, }); + expect(screen.getByTestId('executionsShowing')).toHaveTextContent('Showing 7 rule executions'); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx index 31fc446fa2bb7..b471493a9ecd2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; +import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import React, { useCallback, useMemo, useState } from 'react'; +import moment from 'moment'; +import React, { useCallback, useMemo, useRef } from 'react'; import { EuiTextColor, EuiFlexGroup, @@ -20,11 +20,17 @@ import { EuiSpacer, EuiSwitch, EuiBasicTable, + EuiButton, } from '@elastic/eui'; -import { buildFilter, FILTERS } from '@kbn/es-query'; +import { buildFilter, Filter, FILTERS, Query } from '@kbn/es-query'; import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; +import { mountReactNode } from '@kbn/core/public/utils'; +import { RuleDetailTabs } from '..'; import { RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY } from '../../../../../../../common/constants'; -import { AggregateRuleExecutionEvent } from '../../../../../../../common/detection_engine/schemas/common'; +import { + AggregateRuleExecutionEvent, + RuleExecutionStatus, +} from '../../../../../../../common/detection_engine/schemas/common'; import { UtilityBar, @@ -34,9 +40,23 @@ import { } from '../../../../../../common/components/utility_bar'; import { useSourcererDataView } from '../../../../../../common/containers/sourcerer'; import { useAppToasts } from '../../../../../../common/hooks/use_app_toasts'; +import { useDeepEqualSelector } from '../../../../../../common/hooks/use_selector'; import { useKibana } from '../../../../../../common/lib/kibana'; +import { inputsSelectors } from '../../../../../../common/store'; +import { + setAbsoluteRangeDatePicker, + setFilterQuery, + setRelativeRangeDatePicker, +} from '../../../../../../common/store/inputs/actions'; +import { + AbsoluteTimeRange, + isAbsoluteTimeRange, + isRelativeTimeRange, + RelativeTimeRange, +} from '../../../../../../common/store/inputs/model'; import { SourcererScopeName } from '../../../../../../common/store/sourcerer/model'; import { useRuleExecutionEvents } from '../../../../../containers/detection_engine/rules'; +import { useRuleDetailsContext } from '../rule_details_context'; import * as i18n from './translations'; import { EXECUTION_LOG_COLUMNS, GET_EXECUTION_LOG_METRICS_COLUMNS } from './execution_log_columns'; import { ExecutionLogSearchBar } from './execution_log_search_bar'; @@ -56,6 +76,12 @@ interface ExecutionLogTableProps { selectAlertsTab: () => void; } +interface CachedGlobalQueryState { + filters: Filter[]; + query: Query; + timerange: AbsoluteTimeRange | RelativeTimeRange; +} + const ExecutionLogTableComponent: React.FC = ({ ruleId, selectAlertsTab, @@ -68,28 +94,84 @@ const ExecutionLogTableComponent: React.FC = ({ storage, timelines, } = useKibana().services; - // Datepicker state - const [recentlyUsedRanges, setRecentlyUsedRanges] = useState([]); - const [refreshInterval, setRefreshInterval] = useState(1000); - const [isPaused, setIsPaused] = useState(true); - const [start, setStart] = useState('now-24h'); - const [end, setEnd] = useState('now'); - // Searchbar/Filter/Settings state - const [queryText, setQueryText] = useState(''); - const [statusFilters, setStatusFilters] = useState(undefined); - const [showMetricColumns, setShowMetricColumns] = useState( - storage.get(RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY) ?? false - ); + const { + [RuleDetailTabs.executionLogs]: { + state: { + superDatePicker: { recentlyUsedRanges, refreshInterval, isPaused, start, end }, + queryText, + statusFilters, + showMetricColumns, + pagination: { pageIndex, pageSize }, + sort: { sortField, sortDirection }, + }, + actions: { + setEnd, + setIsPaused, + setPageIndex, + setPageSize, + setQueryText, + setRecentlyUsedRanges, + setRefreshInterval, + setShowMetricColumns, + setSortDirection, + setSortField, + setStart, + setStatusFilters, + }, + }, + } = useRuleDetailsContext(); - // Pagination state - const [pageIndex, setPageIndex] = useState(1); - const [pageSize, setPageSize] = useState(5); - const [sortField, setSortField] = useState('timestamp'); - const [sortDirection, setSortDirection] = useState('desc'); // Index for `add filter` action and toasts for errors const { indexPattern } = useSourcererDataView(SourcererScopeName.detections); - const { addError, addSuccess } = useAppToasts(); + const { addError, addSuccess, remove } = useAppToasts(); + + // QueryString, Filters, and TimeRange state + const dispatch = useDispatch(); + const getGlobalFiltersQuerySelector = useMemo( + () => inputsSelectors.globalFiltersQuerySelector(), + [] + ); + const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); + const timerange = useDeepEqualSelector(inputsSelectors.globalTimeRangeSelector); + const query = useDeepEqualSelector(getGlobalQuerySelector); + const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); + const cachedGlobalQueryState = useRef({ filters, query, timerange }); + const successToastId = useRef(''); + + const resetGlobalQueryState = useCallback(() => { + if (isAbsoluteTimeRange(cachedGlobalQueryState.current.timerange)) { + dispatch( + setAbsoluteRangeDatePicker({ + id: 'global', + from: cachedGlobalQueryState.current.timerange.from, + to: cachedGlobalQueryState.current.timerange.to, + }) + ); + } else if (isRelativeTimeRange(cachedGlobalQueryState.current.timerange)) { + dispatch( + setRelativeRangeDatePicker({ + id: 'global', + from: cachedGlobalQueryState.current.timerange.from, + fromStr: cachedGlobalQueryState.current.timerange.fromStr, + to: cachedGlobalQueryState.current.timerange.to, + toStr: cachedGlobalQueryState.current.timerange.toStr, + }) + ); + } + + dispatch( + setFilterQuery({ + id: 'global', + query: cachedGlobalQueryState.current.query.query, + language: cachedGlobalQueryState.current.query.language, + }) + ); + // Using filterManager directly as dispatch(setSearchBarFilter()) was not replacing filters + filterManager.removeAll(); + filterManager.addFilters(cachedGlobalQueryState.current.filters); + remove(successToastId.current); + }, [dispatch, filterManager, remove]); // Table data state const { @@ -118,15 +200,18 @@ const ExecutionLogTableComponent: React.FC = ({ }, [indexPattern]); // Callbacks - const onTableChangeCallback = useCallback(({ page = {}, sort = {} }) => { - const { index, size } = page; - const { field, direction } = sort; + const onTableChangeCallback = useCallback( + ({ page = {}, sort = {} }) => { + const { index, size } = page; + const { field, direction } = sort; - setPageIndex(index + 1); - setPageSize(size); - setSortField(field); - setSortDirection(direction); - }, []); + setPageIndex(index + 1); + setPageSize(size); + setSortField(field); + setSortDirection(direction); + }, + [setPageIndex, setPageSize, setSortDirection, setSortField] + ); const onTimeChangeCallback = useCallback( (props: OnTimeChangeProps) => { @@ -141,14 +226,17 @@ const ExecutionLogTableComponent: React.FC = ({ recentlyUsedRange.length > 10 ? recentlyUsedRange.slice(0, 9) : recentlyUsedRange ); }, - [recentlyUsedRanges] + [recentlyUsedRanges, setEnd, setRecentlyUsedRanges, setStart] ); - const onRefreshChangeCallback = useCallback((props: OnRefreshChangeProps) => { - setIsPaused(props.isPaused); - // Only support auto-refresh >= 1minute -- no current ability to limit within component - setRefreshInterval(props.refreshInterval > 60000 ? props.refreshInterval : 60000); - }, []); + const onRefreshChangeCallback = useCallback( + (props: OnRefreshChangeProps) => { + setIsPaused(props.isPaused); + // Only support auto-refresh >= 1minute -- no current ability to limit within component + setRefreshInterval(props.refreshInterval > 60000 ? props.refreshInterval : 60000); + }, + [setIsPaused, setRefreshInterval] + ); const onRefreshCallback = useCallback( (props: OnRefreshProps) => { @@ -157,19 +245,26 @@ const ExecutionLogTableComponent: React.FC = ({ [refetch] ); - const onSearchCallback = useCallback((updatedQueryText: string) => { - setQueryText(updatedQueryText); - }, []); + const onSearchCallback = useCallback( + (updatedQueryText: string) => { + setQueryText(updatedQueryText); + }, + [setQueryText] + ); - const onStatusFilterChangeCallback = useCallback((updatedStatusFilters: string[]) => { - setStatusFilters( - updatedStatusFilters.length ? updatedStatusFilters.sort().join(',') : undefined - ); - }, []); + const onStatusFilterChangeCallback = useCallback( + (updatedStatusFilters: RuleExecutionStatus[]) => { + setStatusFilters(updatedStatusFilters); + }, + [setStatusFilters] + ); const onFilterByExecutionIdCallback = useCallback( (executionId: string, executionStart: string) => { if (uuidDataViewField != null) { + // Update cached global query state with current state as a rollback point + cachedGlobalQueryState.current = { filters, query, timerange }; + // Create filter & daterange constraints const filter = buildFilter( indexPattern, uuidDataViewField, @@ -179,20 +274,55 @@ const ExecutionLogTableComponent: React.FC = ({ executionId, null ); + dispatch( + setAbsoluteRangeDatePicker({ + id: 'global', + from: moment(executionStart).subtract(1, 'days').toISOString(), + to: moment(executionStart).add(1, 'days').toISOString(), + }) + ); filterManager.removeAll(); filterManager.addFilters(filter); + dispatch(setFilterQuery({ id: 'global', query: '', language: 'kuery' })); selectAlertsTab(); - addSuccess({ - title: i18n.ACTIONS_SEARCH_FILTERS_HAVE_BEEN_UPDATED_TITLE, - text: i18n.ACTIONS_SEARCH_FILTERS_HAVE_BEEN_UPDATED_DESCRIPTION, - }); + successToastId.current = addSuccess( + { + title: i18n.ACTIONS_SEARCH_FILTERS_HAVE_BEEN_UPDATED_TITLE, + text: mountReactNode( + <> +

{i18n.ACTIONS_SEARCH_FILTERS_HAVE_BEEN_UPDATED_DESCRIPTION}

+ + + + {i18n.ACTIONS_SEARCH_FILTERS_HAVE_BEEN_UPDATED_RESTORE_BUTTON} + + + + + ), + }, + // Essentially keep toast around till user dismisses via 'x' + { toastLifeTimeMs: 10 * 60 * 1000 } + ).id; } else { addError(i18n.ACTIONS_FIELD_NOT_FOUND_ERROR, { title: i18n.ACTIONS_FIELD_NOT_FOUND_ERROR_TITLE, }); } }, - [addError, addSuccess, filterManager, indexPattern, selectAlertsTab, uuidDataViewField] + [ + addError, + addSuccess, + dispatch, + filterManager, + filters, + indexPattern, + query, + resetGlobalQueryState, + selectAlertsTab, + timerange, + uuidDataViewField, + ] ); const onShowMetricColumnsCallback = useCallback( @@ -200,7 +330,7 @@ const ExecutionLogTableComponent: React.FC = ({ storage.set(RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY, showMetrics); setShowMetricColumns(showMetrics); }, - [storage] + [setShowMetricColumns, storage] ); // Memoized state @@ -223,8 +353,6 @@ const ExecutionLogTableComponent: React.FC = ({ }; }, [sortDirection, sortField]); - // TODO: Re-add actions once alert count is displayed in table and UX is finalized - // @ts-expect-error unused constant const actions = useMemo( () => [ { @@ -258,9 +386,9 @@ const ExecutionLogTableComponent: React.FC = ({ const executionLogColumns = useMemo( () => showMetricColumns - ? [...EXECUTION_LOG_COLUMNS, ...GET_EXECUTION_LOG_METRICS_COLUMNS(docLinks)] - : [...EXECUTION_LOG_COLUMNS], - [docLinks, showMetricColumns] + ? [...EXECUTION_LOG_COLUMNS, ...GET_EXECUTION_LOG_METRICS_COLUMNS(docLinks), ...actions] + : [...EXECUTION_LOG_COLUMNS, ...actions], + [actions, docLinks, showMetricColumns] ); return ( @@ -271,6 +399,7 @@ const ExecutionLogTableComponent: React.FC = ({ onSearch={onSearchCallback} onStatusFilterChange={onStatusFilterChangeCallback} onlyShowFilters={true} + defaultSelectedStatusFilters={statusFilters} /> @@ -315,7 +444,7 @@ const ExecutionLogTableComponent: React.FC = ({ - + {timelines.getLastUpdated({ showUpdating: isLoading || isFetching, updatedAt: dataUpdatedAt, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.test.tsx index 44f765f934ae8..30e7d6e8f0a2e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.test.tsx @@ -5,17 +5,82 @@ * 2.0. */ -import { shallow } from 'enzyme'; +import { render } from '@testing-library/react'; import React from 'react'; -import { RuleDurationFormat } from './rule_duration_format'; - -// TODO: Replace snapshot test with base test cases +import { getFormattedDuration, RuleDurationFormat } from './rule_duration_format'; describe('RuleDurationFormat', () => { - describe('snapshots', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + describe('getFormattedDuration', () => { + test('if input value is 0, formatted response is also 0', () => { + const formattedDuration = getFormattedDuration(0); + expect(formattedDuration).toEqual('00:00:00:000'); + }); + + test('if input value only contains ms, formatted response also only contains ms (SSS)', () => { + const formattedDuration = getFormattedDuration(999); + expect(formattedDuration).toEqual('00:00:00:999'); + }); + + test('for milliseconds (SSS) to seconds (ss) overflow', () => { + const formattedDuration = getFormattedDuration(1000); + expect(formattedDuration).toEqual('00:00:01:000'); + }); + + test('for seconds (ss) to minutes (mm) overflow', () => { + const formattedDuration = getFormattedDuration(60000 + 1); + expect(formattedDuration).toEqual('00:01:00:001'); + }); + + test('for minutes (mm) to hours (hh) overflow', () => { + const formattedDuration = getFormattedDuration(60000 * 60 + 1); + expect(formattedDuration).toEqual('01:00:00:001'); + }); + + test('for hours (hh) to days (ddd) overflow', () => { + const formattedDuration = getFormattedDuration(60000 * 60 * 24 + 1); + expect(formattedDuration).toEqual('001:00:00:00:001'); + }); + + test('for overflow with all units up to hours (hh)', () => { + const formattedDuration = getFormattedDuration(25033167); + expect(formattedDuration).toEqual('06:57:13:167'); + }); + + test('for overflow with all units up to hours (ddd)', () => { + const formattedDuration = getFormattedDuration(2503316723); + expect(formattedDuration).toEqual('028:23:21:56:723'); + }); + + test('for overflow greater than a year', () => { + const formattedDuration = getFormattedDuration((60000 * 60 * 24 + 1) * 365); + expect(formattedDuration).toEqual('> 1 Year'); + }); + + test('for max overflow', () => { + const formattedDuration = getFormattedDuration(Number.MAX_SAFE_INTEGER); + expect(formattedDuration).toEqual('> 1 Year'); + }); + }); + + describe('RuleDurationFormatComponent', () => { + test('renders correctly with duration and no additional props', () => { + const { getByTestId } = render(); + expect(getByTestId('rule-duration-format-value')).toHaveTextContent('00:00:01:000'); + }); + + test('renders correctly with duration and isSeconds=true', () => { + const { getByTestId } = render(); + expect(getByTestId('rule-duration-format-value')).toHaveTextContent('00:00:01:000'); + }); + + test('renders correctly with allowZero=true', () => { + const { getByTestId } = render(); + expect(getByTestId('rule-duration-format-value')).toHaveTextContent('N/A'); + }); + + test('renders correctly with max overflow', () => { + const { getByTestId } = render(); + expect(getByTestId('rule-duration-format-value')).toHaveTextContent('> 1 Year'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.tsx index cdbc19ce16c3b..c9f8e1d2734d8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/rule_duration_format.tsx @@ -5,48 +5,59 @@ * 2.0. */ -import numeral from '@elastic/numeral'; import moment from 'moment'; import React, { useMemo } from 'react'; +import * as i18n from './translations'; + interface Props { duration: number; - isMillis?: boolean; + isSeconds?: boolean; allowZero?: boolean; } -export function getFormattedDuration(value: number) { +export const getFormattedDuration = (value: number) => { if (!value) { return '00:00:00:000'; } const duration = moment.duration(value); - const hours = Math.floor(duration.asHours()).toString().padStart(2, '0'); - const minutes = Math.floor(duration.asMinutes()).toString().padStart(2, '0'); + const days = Math.floor(duration.asDays()).toString().padStart(3, '0'); + const hours = Math.floor(duration.asHours() % 24) + .toString() + .padStart(2, '0'); + const minutes = Math.floor(duration.asMinutes() % 60) + .toString() + .padStart(2, '0'); const seconds = duration.seconds().toString().padStart(2, '0'); const ms = duration.milliseconds().toString().padStart(3, '0'); - return `${hours}:${minutes}:${seconds}:${ms}`; -} -export function getFormattedMilliseconds(value: number) { - const formatted = numeral(value).format('0,0'); - return `${formatted} ms`; -} + if (Math.floor(duration.asDays()) > 0) { + if (Math.floor(duration.asDays()) >= 365) { + return i18n.GREATER_THAN_YEAR; + } else { + return `${days}:${hours}:${minutes}:${seconds}:${ms}`; + } + } else { + return `${hours}:${minutes}:${seconds}:${ms}`; + } +}; /** - * Formats duration as (hh:mm:ss:SSS) - * @param props duration default as nanos, set isMillis:true to pass in ms + * Formats duration as (hh:mm:ss:SSS) by default, overflowing to include days + * as (ddd:hh:mm:ss:SSS) if necessary, and then finally to `> 1 Year` + * @param props duration as millis, set isSeconds:true to pass in seconds * @constructor */ const RuleDurationFormatComponent = (props: Props) => { - const { duration, isMillis = false, allowZero = true } = props; + const { duration, isSeconds = false, allowZero = true } = props; const formattedDuration = useMemo(() => { // Durations can be buggy and return negative if (allowZero && duration >= 0) { - return getFormattedDuration(isMillis ? duration * 1000 : duration); + return getFormattedDuration(isSeconds ? duration * 1000 : duration); } - return 'N/A'; - }, [allowZero, duration, isMillis]); + return i18n.DURATION_NOT_AVAILABLE; + }, [allowZero, duration, isSeconds]); return {formattedDuration}; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/translations.ts index d5d8d56664907..b161ae3662e0e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/translations.ts @@ -181,6 +181,13 @@ export const ACTIONS_SEARCH_FILTERS_HAVE_BEEN_UPDATED_DESCRIPTION = i18n.transla } ); +export const ACTIONS_SEARCH_FILTERS_HAVE_BEEN_UPDATED_RESTORE_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.actionSearchFiltersUpdatedRestoreButtonTitle', + { + defaultMessage: 'Restore previous filters', + } +); + export const ACTIONS_FIELD_NOT_FOUND_ERROR_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.actionFieldNotFoundErrorTitle', { @@ -194,3 +201,17 @@ export const ACTIONS_FIELD_NOT_FOUND_ERROR = i18n.translate( defaultMessage: "Cannot find field 'kibana.alert.rule.execution.uuid' in alerts index.", } ); + +export const DURATION_NOT_AVAILABLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.durationNotAvailableDescription', + { + defaultMessage: 'N/A', + } +); + +export const GREATER_THAN_YEAR = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.durationGreaterThanYearDescription', + { + defaultMessage: '> 1 Year', + } +); 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 8550d03eca2aa..cc8872b901f44 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 @@ -108,6 +108,7 @@ import { import * as detectionI18n from '../../translations'; import * as ruleI18n from '../translations'; import { ExecutionLogTable } from './execution_log_table/execution_log_table'; +import { RuleDetailsContextProvider } from './rule_details_context'; import * as i18n from './translations'; import { NeedAdminForUpdateRulesCallOut } from '../../../../components/callouts/need_admin_for_update_callout'; import { MissingPrivilegesCallOut } from '../../../../components/callouts/missing_privileges_callout'; @@ -131,7 +132,14 @@ const StyledFullHeightContainer = styled.div` flex: 1 1 auto; `; -enum RuleDetailTabs { +/** + * Sets min-height on tab container to minimize page hop when switching to tabs with less content + */ +const StyledMinHeightTabContainer = styled.div` + min-height: 800px; +`; + +export enum RuleDetailTabs { alerts = 'alerts', executionLogs = 'executionLogs', exceptions = 'exceptions', @@ -639,181 +647,186 @@ const RuleDetailsPageComponent: React.FC = ({ indexPattern={indexPattern} /> - - - - - - - {ruleStatusI18n.STATUS} - {':'} - - {ruleStatusInfo} - - - } - title={title} - badgeOptions={badgeOptions} - > - - - - - - {i18n.ENABLE_RULE} + + + + + + + {ruleStatusI18n.STATUS} + {':'} + + {ruleStatusInfo} - - - - - - {editRule} - - - - - - - - {ruleError} - {getLegacyUrlConflictCallout} - - - - - - - - - - - {defineRuleData != null && ( - + } + title={title} + badgeOptions={badgeOptions} + > + + + + + - )} - + {i18n.ENABLE_RULE} + + - - - - {scheduleRuleData != null && ( - + + {editRule} + + - )} - + + - - - - {tabs} - - - {ruleDetailTab === RuleDetailTabs.alerts && hasIndexRead && ( - <> - - - + {ruleError} + {getLegacyUrlConflictCallout} + + + + - - {updatedAt && - timelinesUi.getLastUpdated({ - updatedAt: updatedAt || Date.now(), - showUpdating, - })} + + + + + + {defineRuleData != null && ( + + )} + + + + + + {scheduleRuleData != null && ( + + )} + + + - - - - - - {ruleId != null && ( - + {tabs} + + + + {ruleDetailTab === RuleDetailTabs.alerts && hasIndexRead && ( + <> + + + + + + {updatedAt && + timelinesUi.getLastUpdated({ + updatedAt: updatedAt || Date.now(), + showUpdating, + })} + + + + + + + + {ruleId != null && ( + + )} + + )} + {ruleDetailTab === RuleDetailTabs.exceptions && ( + )} - - )} - {ruleDetailTab === RuleDetailTabs.exceptions && ( - - )} - {ruleDetailTab === RuleDetailTabs.executionLogs && ( - - )} - + {ruleDetailTab === RuleDetailTabs.executionLogs && ( + + )} + + + diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/rule_details_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/rule_details_context.tsx new file mode 100644 index 0000000000000..13b17d0493d43 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/rule_details_context.tsx @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; +import React, { createContext, useContext, useMemo, useState } from 'react'; +import { RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY } from '../../../../../../common/constants'; +import { + AggregateRuleExecutionEvent, + RuleExecutionStatus, +} from '../../../../../../common/detection_engine/schemas/common'; +import { invariant } from '../../../../../../common/utils/invariant'; +import { useKibana } from '../../../../../common/lib/kibana'; +import { RuleDetailTabs } from '.'; + +export interface ExecutionLogTableState { + /** + * State of the SuperDatePicker component + */ + superDatePicker: { + /** + * DateRanges to display as recently used + */ + recentlyUsedRanges: DurationRange[]; + /** + * Interval to auto-refresh at + */ + refreshInterval: number; + /** + * State of auto-refresh + */ + isPaused: boolean; + /** + * Start datetime + */ + start: string; + /** + * End datetime + */ + end: string; + }; + /** + * SearchBar query + */ + queryText: string; + /** + * Selected Filters by Execution Status(es) + */ + statusFilters: RuleExecutionStatus[]; + /** + * Whether or not to show additional metric columnbs + */ + showMetricColumns: boolean; + /** + * Currently selected page and number of rows per page + */ + pagination: { + pageIndex: number; + pageSize: number; + }; + sort: { + sortField: keyof AggregateRuleExecutionEvent; + sortDirection: SortOrder; + }; +} + +// @ts-expect-error unused constant +const DEFAULT_STATE: ExecutionLogTableState = { + superDatePicker: { + recentlyUsedRanges: [], + refreshInterval: 1000, + isPaused: true, + start: 'now-24hr', + end: 'now', + }, + queryText: '', + statusFilters: [], + showMetricColumns: false, + pagination: { + pageIndex: 1, + pageSize: 5, + }, + sort: { + sortField: 'timestamp', + sortDirection: 'desc', + }, +}; + +export interface ExecutionLogTableActions { + setRecentlyUsedRanges: React.Dispatch>; + setRefreshInterval: React.Dispatch>; + setIsPaused: React.Dispatch>; + setStart: React.Dispatch>; + setEnd: React.Dispatch>; + setQueryText: React.Dispatch>; + setStatusFilters: React.Dispatch>; + setShowMetricColumns: React.Dispatch>; + setPageIndex: React.Dispatch>; + setPageSize: React.Dispatch>; + setSortField: React.Dispatch>; + setSortDirection: React.Dispatch>; +} + +export interface RuleDetailsContextType { + // TODO: Add section for RuleDetailTabs.exceptions and store query/pagination/etc. + // TODO: Let's discuss how to integration with ExceptionsViewerComponent state mgmt + [RuleDetailTabs.executionLogs]: { + state: ExecutionLogTableState; + actions: ExecutionLogTableActions; + }; +} + +const RuleDetailsContext = createContext(null); + +interface RuleDetailsContextProviderProps { + children: React.ReactNode; +} + +export const RuleDetailsContextProvider = ({ children }: RuleDetailsContextProviderProps) => { + const { storage } = useKibana().services; + + // Execution Log Table tab + // // SuperDatePicker State + const [recentlyUsedRanges, setRecentlyUsedRanges] = useState([]); + const [refreshInterval, setRefreshInterval] = useState(1000); + const [isPaused, setIsPaused] = useState(true); + const [start, setStart] = useState('now-24h'); + const [end, setEnd] = useState('now'); + // Searchbar/Filter/Settings state + const [queryText, setQueryText] = useState(''); + const [statusFilters, setStatusFilters] = useState([]); + const [showMetricColumns, setShowMetricColumns] = useState( + storage.get(RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY) ?? false + ); + // Pagination state + const [pageIndex, setPageIndex] = useState(1); + const [pageSize, setPageSize] = useState(5); + const [sortField, setSortField] = useState('timestamp'); + const [sortDirection, setSortDirection] = useState('desc'); + // // End Execution Log Table tab + + const providerValue = useMemo( + () => ({ + [RuleDetailTabs.executionLogs]: { + state: { + superDatePicker: { + recentlyUsedRanges, + refreshInterval, + isPaused, + start, + end, + }, + queryText, + statusFilters, + showMetricColumns, + pagination: { + pageIndex, + pageSize, + }, + sort: { + sortField, + sortDirection, + }, + }, + actions: { + setEnd, + setIsPaused, + setPageIndex, + setPageSize, + setQueryText, + setRecentlyUsedRanges, + setRefreshInterval, + setShowMetricColumns, + setSortDirection, + setSortField, + setStart, + setStatusFilters, + }, + }, + }), + [ + end, + isPaused, + pageIndex, + pageSize, + queryText, + recentlyUsedRanges, + refreshInterval, + showMetricColumns, + sortDirection, + sortField, + start, + statusFilters, + ] + ); + + return ( + {children} + ); +}; + +export const useRuleDetailsContext = (): RuleDetailsContextType => { + const ruleDetailsContext = useContext(RuleDetailsContext); + invariant( + ruleDetailsContext, + 'useRuleDetailsContext should be used inside RuleDetailsContextProvider' + ); + return ruleDetailsContext; +}; + +export const useRuleDetailsContextOptional = (): RuleDetailsContextType | null => + useContext(RuleDetailsContext); diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.test.tsx deleted file mode 100644 index a96ffb577d90c..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React from 'react'; -import { HostRiskScoreOverTime } from '.'; -import { TestProviders } from '../../../common/mock'; -import { useHostRiskScore } from '../../../risk_score/containers'; - -jest.mock('../../../risk_score/containers'); -const useHostRiskScoreMock = useHostRiskScore as jest.Mock; - -describe('Host Risk Flyout', () => { - it('renders', () => { - useHostRiskScoreMock.mockReturnValueOnce([false, { data: [], isModuleEnabled: true }]); - - const { queryByTestId } = render( - - - - ); - - expect(queryByTestId('hostRiskScoreOverTime')).toBeInTheDocument(); - }); - - it('renders loader when HostsRiskScore is laoding', () => { - useHostRiskScoreMock.mockReturnValueOnce([true, { data: [], isModuleEnabled: true }]); - - const { queryByTestId } = render( - - - - ); - - expect(queryByTestId('HostRiskScoreOverTime-loading')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.tsx deleted file mode 100644 index 52a840e857fff..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.tsx +++ /dev/null @@ -1,210 +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, { useMemo, useCallback } from 'react'; -import { - Chart, - LineSeries, - ScaleType, - Settings, - Axis, - Position, - AnnotationDomainType, - LineAnnotation, - TooltipValue, -} from '@elastic/charts'; -import { euiThemeVars } from '@kbn/ui-theme'; -import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart, EuiText, EuiPanel } from '@elastic/eui'; -import styled from 'styled-components'; -import { chartDefaultSettings, useTheme } from '../../../common/components/charts/common'; -import { useTimeZone } from '../../../common/lib/kibana'; -import { histogramDateTimeFormatter } from '../../../common/components/utils'; -import { HeaderSection } from '../../../common/components/header_section'; -import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; -import * as i18n from './translations'; -import { PreferenceFormattedDate } from '../../../common/components/formatted_date'; -import { useQueryInspector } from '../../../common/components/page/manage_query'; -import { HostsComponentsQueryProps } from '../../pages/navigation/types'; -import { buildHostNamesFilter } from '../../../../common/search_strategy/security_solution/risk_score'; -import { HostRiskScoreQueryId, useHostRiskScore } from '../../../risk_score/containers'; - -export interface HostRiskScoreOverTimeProps - extends Pick { - hostName: string; - from: string; - to: string; -} - -const RISKY_THRESHOLD = 70; -const DEFAULT_CHART_HEIGHT = 250; -const QUERY_ID = HostRiskScoreQueryId.HOST_RISK_SCORE_OVER_TIME; - -const StyledEuiText = styled(EuiText)` - font-size: 9px; - font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold}; - margin-right: ${({ theme }) => theme.eui.paddingSizes.xs}; -`; - -const LoadingChart = styled(EuiLoadingChart)` - display: block; - text-align: center; -`; - -const HostRiskScoreOverTimeComponent: React.FC = ({ - hostName, - from, - to, - setQuery, - deleteQuery, -}) => { - const timeZone = useTimeZone(); - - const dataTimeFormatter = useMemo(() => histogramDateTimeFormatter([from, to]), [from, to]); - const scoreFormatter = useCallback((d: number) => Math.round(d).toString(), []); - const headerFormatter = useCallback( - (tooltip: TooltipValue) => , - [] - ); - - const timerange = useMemo( - () => ({ - from, - to, - }), - [from, to] - ); - const theme = useTheme(); - - const [loading, { data, refetch, inspect }] = useHostRiskScore({ - filterQuery: hostName ? buildHostNamesFilter([hostName]) : undefined, - onlyLatest: false, - timerange, - }); - - const graphData = useMemo( - () => - data - ?.map((hostRisk) => ({ - x: hostRisk['@timestamp'], - y: hostRisk.risk_stats.risk_score, - })) - .reverse() ?? [], - [data] - ); - - useQueryInspector({ - queryId: QUERY_ID, - loading, - refetch, - setQuery, - deleteQuery, - inspect, - }); - - return ( - - - - - - - - - - - - - - -
- {loading ? ( - - ) : ( - - - - - - - {i18n.RISKY} - - } - /> - - )} -
-
-
-
-
- ); -}; - -HostRiskScoreOverTimeComponent.displayName = 'HostRiskScoreOverTimeComponent'; -export const HostRiskScoreOverTime = React.memo(HostRiskScoreOverTimeComponent); -HostRiskScoreOverTime.displayName = 'HostRiskScoreOverTime'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx deleted file mode 100644 index 5ff8696ae5be3..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx +++ /dev/null @@ -1,150 +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 { render, fireEvent } from '@testing-library/react'; -import React from 'react'; -import { TopHostScoreContributors } from '.'; -import { TestProviders } from '../../../common/mock'; -import { useHostRiskScore } from '../../../risk_score/containers'; -import { useQueryToggle } from '../../../common/containers/query_toggle'; - -jest.mock('../../../common/containers/query_toggle'); -jest.mock('../../../risk_score/containers'); -const useHostRiskScoreMock = useHostRiskScore as jest.Mock; -const testProps = { - setQuery: jest.fn(), - deleteQuery: jest.fn(), - hostName: 'test-host-name', - from: '2020-07-07T08:20:18.966Z', - to: '2020-07-08T08:20:18.966Z', -}; -describe('Host Risk Flyout', () => { - const mockUseQueryToggle = useQueryToggle as jest.Mock; - const mockSetToggle = jest.fn(); - beforeEach(() => { - jest.clearAllMocks(); - mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); - }); - it('renders', () => { - useHostRiskScoreMock.mockReturnValueOnce([ - true, - { - data: [], - isModuleEnabled: true, - }, - ]); - - const { queryByTestId } = render( - - - - ); - - expect(queryByTestId('topHostScoreContributors')).toBeInTheDocument(); - }); - - it('renders sorted items', () => { - useHostRiskScoreMock.mockReturnValueOnce([ - true, - { - data: [ - { - risk_stats: { - rule_risks: [ - { - rule_name: 'third', - rule_risk: '10', - }, - { - rule_name: 'first', - rule_risk: '99', - }, - { - rule_name: 'second', - rule_risk: '55', - }, - ], - }, - }, - ], - isModuleEnabled: true, - }, - ]); - - const { queryAllByRole } = render( - - - - ); - - expect(queryAllByRole('row')[1]).toHaveTextContent('first'); - expect(queryAllByRole('row')[2]).toHaveTextContent('second'); - expect(queryAllByRole('row')[3]).toHaveTextContent('third'); - }); - - describe('toggleQuery', () => { - beforeEach(() => { - useHostRiskScoreMock.mockReturnValue([ - true, - { - data: [], - isModuleEnabled: true, - }, - ]); - }); - - test('toggleQuery updates toggleStatus', () => { - const { getByTestId } = render( - - - - ); - expect(useHostRiskScoreMock.mock.calls[0][0].skip).toEqual(false); - fireEvent.click(getByTestId('query-toggle-header')); - expect(mockSetToggle).toBeCalledWith(false); - expect(useHostRiskScoreMock.mock.calls[1][0].skip).toEqual(true); - }); - - test('toggleStatus=true, do not skip', () => { - render( - - - - ); - expect(useHostRiskScoreMock.mock.calls[0][0].skip).toEqual(false); - }); - - test('toggleStatus=true, render components', () => { - const { queryByTestId } = render( - - - - ); - expect(queryByTestId('topHostScoreContributors-table')).toBeTruthy(); - }); - - test('toggleStatus=false, do not render components', () => { - mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); - const { queryByTestId } = render( - - - - ); - expect(queryByTestId('topHostScoreContributors-table')).toBeFalsy(); - }); - - test('toggleStatus=false, skip', () => { - mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); - render( - - - - ); - expect(useHostRiskScoreMock.mock.calls[0][0].skip).toEqual(true); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx deleted file mode 100644 index ceb4394619fc5..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx +++ /dev/null @@ -1,176 +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, { useCallback, useEffect, useMemo, useState } from 'react'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiInMemoryTable, - EuiTableFieldDataColumnType, -} from '@elastic/eui'; - -import { Direction } from '@kbn/timelines-plugin/common'; -import { HeaderSection } from '../../../common/components/header_section'; -import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; -import * as i18n from './translations'; - -import { buildHostNamesFilter, RiskScoreFields } from '../../../../common/search_strategy'; - -import { useQueryInspector } from '../../../common/components/page/manage_query'; -import { HostsComponentsQueryProps } from '../../pages/navigation/types'; - -import { RuleLink } from '../../../detections/pages/detection_engine/rules/all/use_columns'; -import { HostRiskScoreQueryId, useHostRiskScore } from '../../../risk_score/containers'; -import { useQueryToggle } from '../../../common/containers/query_toggle'; - -export interface TopHostScoreContributorsProps - extends Pick { - hostName: string; - from: string; - to: string; -} -interface TableItem { - rank: number; - name: string; - id: string; -} - -const columns: Array> = [ - { - name: i18n.RANK_TITLE, - field: 'rank', - width: '45px', - align: 'right', - }, - { - name: i18n.RULE_NAME_TITLE, - field: 'name', - sortable: true, - truncateText: true, - render: (value: TableItem['name'], { id }: TableItem) => - id ? : value, - }, -]; - -const PAGE_SIZE = 5; -const QUERY_ID = HostRiskScoreQueryId.TOP_HOST_SCORE_CONTRIBUTORS; - -const TopHostScoreContributorsComponent: React.FC = ({ - hostName, - from, - to, - setQuery, - deleteQuery, -}) => { - const timerange = useMemo( - () => ({ - from, - to, - }), - [from, to] - ); - - const sort = useMemo(() => ({ field: RiskScoreFields.timestamp, direction: Direction.desc }), []); - - const { toggleStatus, setToggleStatus } = useQueryToggle(QUERY_ID); - const [querySkip, setQuerySkip] = useState(!toggleStatus); - useEffect(() => { - setQuerySkip(!toggleStatus); - }, [toggleStatus]); - - const toggleQuery = useCallback( - (status: boolean) => { - setToggleStatus(status); - // toggle on = skipQuery false - setQuerySkip(!status); - }, - [setQuerySkip, setToggleStatus] - ); - - const [loading, { data, refetch, inspect }] = useHostRiskScore({ - filterQuery: hostName ? buildHostNamesFilter([hostName]) : undefined, - timerange, - onlyLatest: false, - sort, - skip: querySkip, - pagination: { - querySize: 1, - cursorStart: 0, - }, - }); - - const items = useMemo(() => { - const rules = data && data.length > 0 ? data[0].risk_stats.rule_risks : []; - - return rules - .sort((a, b) => b.rule_risk - a.rule_risk) - .map(({ rule_name: name, rule_id: id }, i) => ({ rank: i + 1, name, id })); - }, [data]); - - const tablePagination = useMemo( - () => ({ - showPerPageOptions: false, - pageSize: PAGE_SIZE, - totalItemCount: items.length, - }), - [items.length] - ); - - useQueryInspector({ - queryId: QUERY_ID, - loading, - refetch, - setQuery, - deleteQuery, - inspect, - }); - - return ( - - - - - - - {toggleStatus && ( - - - - )} - - - {toggleStatus && ( - - - - - - )} - - - ); -}; - -export const TopHostScoreContributors = React.memo(TopHostScoreContributorsComponent); -TopHostScoreContributors.displayName = 'TopHostScoreContributors'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.test.tsx new file mode 100644 index 0000000000000..bab6809afc6f6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.test.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../common/mock'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; + +import { useHostRiskScore } from '../../../risk_score/containers'; +import { HostRiskTabBody } from './host_risk_tab_body'; +import { HostsType } from '../../store/model'; + +jest.mock('../../../risk_score/containers'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('../../../common/lib/kibana'); + +describe('Host query tab body', () => { + const mockUseUserRiskScore = useHostRiskScore as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + hostName: 'testUser', + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: HostsType.page, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + mockUseUserRiskScore.mockReturnValue([ + false, + { + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + refetch: jest.fn(), + isModuleEnabled: true, + }, + ]); + }); + + it("doesn't skip when both toggleStatus are true", () => { + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: true, setToggleStatus: jest.fn() }); + + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(false); + }); + + it("doesn't skip when at least one toggleStatus is true", () => { + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: false, setToggleStatus: jest.fn() }); + + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(false); + }); + + it('does skip when both toggleStatus are false', () => { + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: false, setToggleStatus: jest.fn() }); + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: false, setToggleStatus: jest.fn() }); + + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx index cebcc0ee855ea..b23ebb7de9bef 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx @@ -6,19 +6,26 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { HostRiskScoreOverTime } from '../../components/host_score_over_time'; -import { TopHostScoreContributors } from '../../components/top_host_score_contributors'; + import { HostsComponentsQueryProps } from './types'; import * as i18n from '../translations'; import { useRiskyHostsDashboardButtonHref } from '../../../overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href'; import { HostRiskInformationButtonEmpty } from '../../components/host_risk_information'; +import { HostRiskScoreQueryId, useHostRiskScore } from '../../../risk_score/containers'; +import { buildHostNamesFilter } from '../../../../common/search_strategy'; +import { useQueryInspector } from '../../../common/components/page/manage_query'; +import { RiskScoreOverTime } from '../../../common/components/risk_score_over_time'; +import { TopRiskScoreContributors } from '../../../common/components/top_risk_score_contributors'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const StyledEuiFlexGroup = styled(EuiFlexGroup)` margin-top: ${({ theme }) => theme.eui.paddingSizes.l}; `; +const QUERY_ID = HostRiskScoreQueryId.HOST_DETAILS_RISK_SCORE; + const HostRiskTabBodyComponent: React.FC< Pick & { hostName: string; @@ -26,25 +33,74 @@ const HostRiskTabBodyComponent: React.FC< > = ({ hostName, startDate, endDate, setQuery, deleteQuery }) => { const { buttonHref } = useRiskyHostsDashboardButtonHref(startDate, endDate); + const timerange = useMemo( + () => ({ + from: startDate, + to: endDate, + }), + [startDate, endDate] + ); + + const { toggleStatus: overTimeToggleStatus, setToggleStatus: setOverTimeToggleStatus } = + useQueryToggle(`${QUERY_ID} overTime`); + const { toggleStatus: contributorsToggleStatus, setToggleStatus: setContributorsToggleStatus } = + useQueryToggle(`${QUERY_ID} contributors`); + + const [loading, { data, refetch, inspect }] = useHostRiskScore({ + filterQuery: hostName ? buildHostNamesFilter([hostName]) : undefined, + onlyLatest: false, + skip: !overTimeToggleStatus && !contributorsToggleStatus, + timerange, + }); + + useQueryInspector({ + queryId: QUERY_ID, + loading, + refetch, + setQuery, + deleteQuery, + inspect, + }); + + const toggleContributorsQuery = useCallback( + (status: boolean) => { + setContributorsToggleStatus(status); + }, + [setContributorsToggleStatus] + ); + + const toggleOverTimeQuery = useCallback( + (status: boolean) => { + setOverTimeToggleStatus(status); + }, + [setOverTimeToggleStatus] + ); + + const rules = data && data.length > 0 ? data[data.length - 1].risk_stats.rule_risks : []; + return ( <> - + - diff --git a/x-pack/plugins/security_solution/public/hosts/pages/translations.ts b/x-pack/plugins/security_solution/public/hosts/pages/translations.ts index 8b92ef035405f..3e6b23a521026 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/translations.ts @@ -60,7 +60,7 @@ export const NAVIGATION_ALERTS_TITLE = i18n.translate( export const NAVIGATION_HOST_RISK_TITLE = i18n.translate( 'xpack.securitySolution.hosts.navigation.hostRisk', { - defaultMessage: 'Hosts by risk', + defaultMessage: 'Host risk', } ); @@ -97,3 +97,10 @@ export const VIEW_DASHBOARD_BUTTON = i18n.translate( defaultMessage: 'View source dashboard', } ); + +export const HOST_RISK_SCORE_OVER_TIME = i18n.translate( + 'xpack.securitySolution.hosts.navigaton.hostScoreOverTimeTitle', + { + defaultMessage: 'Host risk score over time', + } +); diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx new file mode 100644 index 0000000000000..3553f44cc621f --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { SecurityPageName } from '../../app/types'; +import { TestProviders } from '../../common/mock'; +import { LandingLinksIcons, NavItem } from './landing_links_icons'; + +const DEFAULT_NAV_ITEM: NavItem = { + id: SecurityPageName.overview, + label: 'TEST LABEL', + description: 'TEST DESCRIPTION', + icon: 'myTestIcon', +}; + +const mockNavigateTo = jest.fn(); +jest.mock('../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../common/lib/kibana'); + return { + ...originalModule, + useNavigation: () => ({ + navigateTo: mockNavigateTo, + }), + }; +}); + +jest.mock('../../common/components/link_to', () => { + const originalModule = jest.requireActual('../../common/components/link_to'); + return { + ...originalModule, + useFormatUrl: (id: string) => ({ + formatUrl: jest.fn().mockImplementation((path: string) => `/${id}`), + search: '', + }), + }; +}); + +describe('LandingLinksIcons', () => { + it('renders', () => { + const label = 'test label'; + + const { queryByText } = render( + + + + ); + + expect(queryByText(label)).toBeInTheDocument(); + }); + + it('renders navigation link', () => { + const id = SecurityPageName.administration; + const label = 'myTestLable'; + + const { getByText } = render( + + + + ); + + fireEvent.click(getByText(label)); + + expect(mockNavigateTo).toHaveBeenCalledWith({ url: '/administration' }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.tsx new file mode 100644 index 0000000000000..82a0d2148f683 --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.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 { + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, + EuiTitle, + IconType, +} from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { SecurityPageName } from '../../app/types'; +import { + SecuritySolutionLinkAnchor, + withSecuritySolutionLink, +} from '../../common/components/links'; + +interface LandingLinksImagesProps { + items: NavItem[]; +} + +export interface NavItem { + id: SecurityPageName; + label: string; + icon: IconType; + description: string; + path?: string; +} + +const Link = styled.a` + color: inherit; +`; + +const SecuritySolutionLink = withSecuritySolutionLink(Link); + +const Description = styled(EuiFlexItem)` + max-width: 22em; +`; + +const StyledEuiTitle = styled(EuiTitle)` + margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; + margin-bottom: ${({ theme }) => theme.eui.paddingSizes.xs}; +`; + +export const LandingLinksIcons: React.FC = ({ items }) => ( + + {items.map(({ label, description, path, id, icon }) => ( + + + + + + + + + +

{label}

+
+
+
+ + + {description} + + +
+
+ ))} +
+); diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx index d326fff1a9262..479de5e13f432 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { SecurityPageName } from '../../app/types'; import { TestProviders } from '../../common/mock'; import { LandingLinksImages, NavItem } from './landing_links_images'; -import { SCREENSHOT_IMAGE_ALT } from './translations'; const DEFAULT_NAV_ITEM: NavItem = { id: SecurityPageName.overview, @@ -32,7 +31,7 @@ jest.mock('../../common/lib/kibana/kibana_react', () => { }); describe('LandingLinksImages', () => { - test('renders', () => { + it('renders', () => { const label = 'test label'; const { queryByText } = render( @@ -44,16 +43,16 @@ describe('LandingLinksImages', () => { expect(queryByText(label)).toBeInTheDocument(); }); - test('renders image', () => { + it('renders image', () => { const image = 'test_image.jpeg'; const label = 'TEST_LABEL'; - const { getByAltText } = render( + const { getByTestId } = render( ); - expect(getByAltText(SCREENSHOT_IMAGE_ALT(label))).toHaveAttribute('src', image); + expect(getByTestId('LandingLinksImage')).toHaveAttribute('src', image); }); }); diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx index 93366b413e0bc..b6a16da8cdc82 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx @@ -9,7 +9,6 @@ import React from 'react'; import styled from 'styled-components'; import { SecurityPageName } from '../../app/types'; import { withSecuritySolutionLink } from '../../common/components/links'; -import * as i18n from './translations'; interface LandingLinksImagesProps { items: NavItem[]; @@ -23,7 +22,7 @@ export interface NavItem { path?: string; } -const PrimatyEuiTitle = styled(EuiTitle)` +const PrimaryEuiTitle = styled(EuiTitle)` color: ${(props) => props.theme.eui.euiColorPrimary}; `; @@ -49,18 +48,24 @@ const Content = styled(EuiFlexItem)` export const LandingLinksImages: React.FC = ({ items }) => ( {items.map(({ label, description, path, image, id }) => ( - - + + {/* Empty onClick is to force hover style on `EuiPanel` */} {}}> - + - +

{label}

-
+ {description} diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/translations.ts b/x-pack/plugins/security_solution/public/landing_pages/components/translations.ts deleted file mode 100644 index 6f27c3e58cbbc..0000000000000 --- a/x-pack/plugins/security_solution/public/landing_pages/components/translations.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const SCREENSHOT_IMAGE_ALT = (pageName: string) => - i18n.translate('xpack.securitySolution.landing.threatHunting.pageImageAlt', { - values: { pageName }, - defaultMessage: '{pageName} page screenshot', - }); diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/blocklist.tsx b/x-pack/plugins/security_solution/public/landing_pages/icons/blocklist.tsx new file mode 100644 index 0000000000000..75d272034d668 --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/icons/blocklist.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { SVGProps } from 'react'; +export const IconBlocklist: React.FC> = ({ ...props }) => ( + + + + + + + +); diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/endpoint_policies.tsx b/x-pack/plugins/security_solution/public/landing_pages/icons/endpoint_policies.tsx new file mode 100644 index 0000000000000..7563ac17045a2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/icons/endpoint_policies.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { SVGProps } from 'react'; +export const IconEndpointPolicies: React.FC> = ({ ...props }) => ( + + + +); diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/endpoints.tsx b/x-pack/plugins/security_solution/public/landing_pages/icons/endpoints.tsx new file mode 100644 index 0000000000000..a8254d3996c72 --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/icons/endpoints.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 React, { SVGProps } from 'react'; +export const IconEndpoints: React.FC> = ({ ...props }) => ( + + + + + +); diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/event_filters.tsx b/x-pack/plugins/security_solution/public/landing_pages/icons/event_filters.tsx new file mode 100644 index 0000000000000..78628b7904e8a --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/icons/event_filters.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, { SVGProps } from 'react'; +export const IconEventFilters: React.FC> = ({ ...props }) => ( + + + + + + +); diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/exception_lists.tsx b/x-pack/plugins/security_solution/public/landing_pages/icons/exception_lists.tsx new file mode 100644 index 0000000000000..a4cc7a25a9785 --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/icons/exception_lists.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { SVGProps } from 'react'; +export const IconExceptionLists: React.FC> = ({ ...props }) => ( + + + + + + +); diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/host_isolation.tsx b/x-pack/plugins/security_solution/public/landing_pages/icons/host_isolation.tsx new file mode 100644 index 0000000000000..c0df14075c958 --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/icons/host_isolation.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, { SVGProps } from 'react'; +export const IconHostIsolation: React.FC> = ({ ...props }) => ( + + + + + + + + + + + + + +); diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/siem_rules.tsx b/x-pack/plugins/security_solution/public/landing_pages/icons/siem_rules.tsx new file mode 100644 index 0000000000000..fe89c8fb22581 --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/icons/siem_rules.tsx @@ -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 React, { SVGProps } from 'react'; +export const IconSiemRules: React.FC> = ({ ...props }) => ( + + + + + + + + + + + + + + + + + +); diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/trusted_applications.tsx b/x-pack/plugins/security_solution/public/landing_pages/icons/trusted_applications.tsx new file mode 100644 index 0000000000000..7b9f980b7f231 --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/icons/trusted_applications.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 React, { SVGProps } from 'react'; +export const IconTrustedApplications: React.FC> = ({ ...props }) => ( + + + + + + + + +); diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx new file mode 100644 index 0000000000000..efb1bcf35c39e --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.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 { render } from '@testing-library/react'; +import React from 'react'; +import { SecurityPageName } from '../../app/types'; +import { TestProviders } from '../../common/mock'; +import { LandingCategories, NavConfigType } from './manage'; + +const RULES_ITEM_LABEL = 'elastic rules!'; +const EXCEPTIONS_ITEM_LABEL = 'exceptional!'; + +const testConfig: NavConfigType = { + categories: [ + { + label: 'first tests category', + itemIds: [SecurityPageName.rules], + }, + { + label: 'second tests category', + itemIds: [SecurityPageName.exceptions], + }, + ], + items: [ + { + id: SecurityPageName.rules, + label: RULES_ITEM_LABEL, + description: '', + icon: 'testIcon1', + }, + { + id: SecurityPageName.exceptions, + label: EXCEPTIONS_ITEM_LABEL, + description: '', + icon: 'testIcon2', + }, + ], +}; + +describe('LandingCategories', () => { + it('renders items', () => { + const { queryByText } = render( + + + + ); + + expect(queryByText(RULES_ITEM_LABEL)).toBeInTheDocument(); + expect(queryByText(EXCEPTIONS_ITEM_LABEL)).toBeInTheDocument(); + }); + + it('renders items in the same order as defined', () => { + const { queryAllByTestId } = render( + + + + ); + + const renderedItems = queryAllByTestId('LandingItem'); + + expect(renderedItems[0]).toHaveTextContent(EXCEPTIONS_ITEM_LABEL); + expect(renderedItems[1]).toHaveTextContent(RULES_ITEM_LABEL); + }); +}); diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx new file mode 100644 index 0000000000000..da4d25f621305 --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiHorizontalRule, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { compact } from 'lodash/fp'; +import React from 'react'; +import styled from 'styled-components'; +import { + BLOCKLIST, + ENDPOINTS, + EVENT_FILTERS, + EXCEPTIONS, + TRUSTED_APPLICATIONS, +} from '../../app/translations'; +import { SecurityPageName } from '../../app/types'; +import { HeaderPage } from '../../common/components/header_page'; +import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { LandingLinksIcons, NavItem } from '../components/landing_links_icons'; +import { IconBlocklist } from '../icons/blocklist'; +import { IconEndpoints } from '../icons/endpoints'; +import { IconEndpointPolicies } from '../icons/endpoint_policies'; +import { IconEventFilters } from '../icons/event_filters'; +import { IconExceptionLists } from '../icons/exception_lists'; +import { IconHostIsolation } from '../icons/host_isolation'; +import { IconSiemRules } from '../icons/siem_rules'; +import { IconTrustedApplications } from '../icons/trusted_applications'; +import { MANAGE_PAGE_TITLE } from './translations'; + +// TODO +const FIX_ME_TEMPORARY_DESCRIPTION = 'Description here'; + +export interface NavConfigType { + items: NavItem[]; + categories: Array<{ label: string; itemIds: SecurityPageName[] }>; +} + +const config: NavConfigType = { + categories: [ + { + label: i18n.translate('xpack.securitySolution.landing.threatHunting.siemTitle', { + defaultMessage: 'SIEM', + }), + itemIds: [SecurityPageName.rules, SecurityPageName.exceptions], + }, + { + label: i18n.translate('xpack.securitySolution.landing.threatHunting.endpointsTitle', { + defaultMessage: 'ENDPOINTS', + }), + itemIds: [ + SecurityPageName.endpoints, + SecurityPageName.policies, + SecurityPageName.trustedApps, + SecurityPageName.eventFilters, + SecurityPageName.blocklist, + SecurityPageName.hostIsolationExceptions, + ], + }, + ], + items: [ + { + id: SecurityPageName.rules, + label: i18n.translate('xpack.securitySolution.landing.manage.rulesLabel', { + defaultMessage: 'SIEM rules', + }), + description: FIX_ME_TEMPORARY_DESCRIPTION, + icon: IconSiemRules, + }, + { + id: SecurityPageName.exceptions, + label: EXCEPTIONS, + description: FIX_ME_TEMPORARY_DESCRIPTION, + icon: IconExceptionLists, + }, + { + id: SecurityPageName.endpoints, + label: ENDPOINTS, + description: i18n.translate('xpack.securitySolution.landing.manage.endpointsDescription', { + defaultMessage: 'Hosts running endpoint security', + }), + icon: IconEndpoints, + }, + { + id: SecurityPageName.policies, + label: i18n.translate('xpack.securitySolution.landing.manage.endpointPoliceLabel', { + defaultMessage: 'Endpoint policies', + }), + description: FIX_ME_TEMPORARY_DESCRIPTION, + icon: IconEndpointPolicies, + }, + { + id: SecurityPageName.trustedApps, + label: TRUSTED_APPLICATIONS, + description: i18n.translate( + 'xpack.securitySolution.landing.manage.trustedApplicationsDescription', + { + defaultMessage: + 'Improve performance or alleviate conflicts with other applications running on your hosts', + } + ), + icon: IconTrustedApplications, + }, + { + id: SecurityPageName.eventFilters, + label: EVENT_FILTERS, + description: i18n.translate('xpack.securitySolution.landing.manage.eventFiltersDescription', { + defaultMessage: 'Exclude unwanted applications from running on your hosts', + }), + icon: IconEventFilters, + }, + { + id: SecurityPageName.blocklist, + label: BLOCKLIST, + description: FIX_ME_TEMPORARY_DESCRIPTION, + icon: IconBlocklist, + }, + { + id: SecurityPageName.hostIsolationExceptions, + label: i18n.translate('xpack.securitySolution.landing.manage.hostIsolationLabel', { + defaultMessage: 'Host isolation IP exceptions', + }), + description: i18n.translate( + 'xpack.securitySolution.landing.manage.hostIsolationDescription', + { + defaultMessage: 'Allow isolated hosts to communicate with specific IPs', + } + ), + + icon: IconHostIsolation, + }, + ], +}; + +export const ManageLandingPage = () => ( + + + + + +); + +const StyledEuiHorizontalRule = styled(EuiHorizontalRule)` + margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; + margin-bottom: ${({ theme }) => theme.eui.paddingSizes.l}; +`; + +const getNavItembyId = (navConfig: NavConfigType) => (itemId: string) => + navConfig.items.find(({ id }: NavItem) => id === itemId); + +const navItemsFromIds = (itemIds: SecurityPageName[], navConfig: NavConfigType) => + compact(itemIds.map(getNavItembyId(navConfig))); + +export const LandingCategories = React.memo(({ navConfig }: { navConfig: NavConfigType }) => { + return ( + <> + {navConfig.categories.map(({ label, itemIds }, index) => ( +
+ {index > 0 && ( + <> + + + + )} + +

{label}

+
+ + +
+ ))} + + ); +}); + +LandingCategories.displayName = 'LandingCategories'; diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/translations.ts b/x-pack/plugins/security_solution/public/landing_pages/pages/translations.ts index 398bc3fc89fd8..13a2396201cc5 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/translations.ts @@ -20,3 +20,7 @@ export const DASHBOARDS_PAGE_TITLE = i18n.translate( defaultMessage: 'Dashboards', } ); + +export const MANAGE_PAGE_TITLE = i18n.translate('xpack.securitySolution.landing.manage.pageTitle', { + defaultMessage: 'Manage', +}); diff --git a/x-pack/plugins/security_solution/public/landing_pages/routes.tsx b/x-pack/plugins/security_solution/public/landing_pages/routes.tsx index d849b4c22cf7e..af8ce9dbdaf2a 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/routes.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/routes.tsx @@ -9,9 +9,10 @@ import React from 'react'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { SecurityPageName, SecuritySubPluginRoutes } from '../app/types'; -import { DASHBOARDS_PATH, THREAT_HUNTING_PATH } from '../../common/constants'; +import { DASHBOARDS_PATH, MANAGE_PATH, THREAT_HUNTING_PATH } from '../../common/constants'; import { ThreatHuntingLandingPage } from './pages/threat_hunting'; import { DashboardsLandingPage } from './pages/dashboards'; +import { ManageLandingPage } from './pages/manage'; export const ThreatHuntingRoutes = () => ( @@ -25,6 +26,12 @@ export const DashboardRoutes = () => ( ); +export const ManageRoutes = () => ( + + + +); + export const routes: SecuritySubPluginRoutes = [ { path: THREAT_HUNTING_PATH, @@ -34,4 +41,8 @@ export const routes: SecuritySubPluginRoutes = [ path: DASHBOARDS_PATH, render: DashboardRoutes, }, + { + path: MANAGE_PATH, + render: ManageRoutes, + }, ]; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx index 8ff4b71668fd4..2f4b4d241f48c 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/bad_argument.tsx @@ -5,11 +5,10 @@ * 2.0. */ -import React, { memo, PropsWithChildren } from 'react'; -import { EuiCallOut, EuiText } from '@elastic/eui'; -import { UserCommandInput } from './user_command_input'; +import React, { memo, PropsWithChildren, useEffect } from 'react'; +import { EuiCallOut } from '@elastic/eui'; import { ParsedCommandInput } from '../service/parsed_command_input'; -import { CommandDefinition } from '../types'; +import { CommandDefinition, CommandExecutionComponentProps } from '../types'; import { CommandInputUsage } from './command_usage'; import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; @@ -19,21 +18,22 @@ export type BadArgumentProps = PropsWithChildren<{ commandDefinition: CommandDefinition; }>; -export const BadArgument = memo( - ({ parsedInput, commandDefinition, children = null }) => { - const getTestId = useTestIdGenerator(useDataTestSubj()); +/** + * Shows a bad argument error. The error message needs to be defined via the Command History Item's + * `state.errorMessage` + */ +export const BadArgument = memo(({ command, setStatus, store }) => { + const getTestId = useTestIdGenerator(useDataTestSubj()); + + useEffect(() => { + setStatus('success'); + }, [setStatus]); - return ( - <> - - - - - {children} - - - - ); - } -); + return ( + + {store.errorMessage} + + + ); +}); BadArgument.displayName = 'BadArgument'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/clear_command.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/clear_command.tsx new file mode 100644 index 0000000000000..bfa06f55d2665 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/clear_command.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { memo, useEffect } from 'react'; +import { useConsoleStateDispatch } from '../../hooks/state_selectors/use_console_state_dispatch'; +import { CommandExecutionComponentProps } from '../../types'; + +export const ClearCommand = memo(({ status, setStatus }) => { + const dispatch = useConsoleStateDispatch(); + + useEffect(() => { + if (status === 'pending') { + dispatch({ type: 'clear' }); + } + setStatus('success'); + }, [status, setStatus, dispatch]); + + return null; +}); +ClearCommand.displayName = 'ClearCommand'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/help_command.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/help_command.tsx new file mode 100644 index 0000000000000..f8c66f31e396d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/help_command.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 { i18n } from '@kbn/i18n'; +import React, { memo, useEffect } from 'react'; +import { useWithCustomHelpComponent } from '../../hooks/state_selectors/use_with_custom_help_component'; +import { CommandList } from '../command_list'; +import { useWithCommandList } from '../../hooks/state_selectors/use_with_command_list'; +import type { CommandExecutionComponentProps } from '../../types'; +import { HelpOutput } from '../help_output'; + +export const HelpCommand = memo((props) => { + const commands = useWithCommandList(); + const CustomHelpComponent = useWithCustomHelpComponent(); + + useEffect(() => { + if (!CustomHelpComponent) { + props.setStatus('success'); + } + }, [CustomHelpComponent, props]); + + return CustomHelpComponent ? ( + + ) : ( + + + + ); +}); +HelpCommand.displayName = 'HelpCommand'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/help_command_argument.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/help_command_argument.tsx new file mode 100644 index 0000000000000..f67c44013d059 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/console/components/builtin_commands/help_command_argument.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 React, { memo, useEffect } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { CommandUsage } from '../command_usage'; +import { HelpOutput } from '../help_output'; +import { CommandExecutionComponentProps } from '../../types'; + +/** + * Builtin component that handles the output of command's `--help` argument + */ +export const HelpCommandArgument = memo((props) => { + const CustomCommandHelp = props.command.commandDefinition.HelpComponent; + + useEffect(() => { + if (!CustomCommandHelp) { + props.setStatus('success'); + } + }, [CustomCommandHelp, props]); + + return CustomCommandHelp ? ( + + ) : ( + + + + ); +}); +HelpCommandArgument.displayName = 'HelpCommandArgument'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_output.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_output.tsx index 8bb9769980914..8a6611ffbbb18 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_output.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/command_execution_output.tsx @@ -5,103 +5,74 @@ * 2.0. */ -import React, { memo, ReactNode, useCallback, useEffect, useState } from 'react'; -import { EuiButton, EuiLoadingChart } from '@elastic/eui'; +import React, { memo, useCallback, useMemo } from 'react'; +import { EuiLoadingChart } from '@elastic/eui'; import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { CommandExecutionFailure } from './command_execution_failure'; +import type { CommandExecutionState, CommandHistoryItem } from './console_state/types'; import { UserCommandInput } from './user_command_input'; -import { Command } from '../types'; -import { useCommandService } from '../hooks/state_selectors/use_command_service'; import { useConsoleStateDispatch } from '../hooks/state_selectors/use_console_state_dispatch'; const CommandOutputContainer = styled.div` position: relative; - - .run-in-background { - position: absolute; - right: 0; - top: 1em; - } `; export interface CommandExecutionOutputProps { - command: Command; + item: CommandHistoryItem; } -export const CommandExecutionOutput = memo(({ command }) => { - const commandService = useCommandService(); - const [isRunning, setIsRunning] = useState(true); - const [output, setOutput] = useState(null); - const dispatch = useConsoleStateDispatch(); - - // FIXME:PT implement the `run in the background` functionality - const [showRunInBackground, setShowRunInTheBackground] = useState(false); - const handleRunInBackgroundClick = useCallback(() => { - setShowRunInTheBackground(false); - }, []); - - useEffect(() => { - (async () => { - const timeoutId = setTimeout(() => { - setShowRunInTheBackground(true); - }, 15000); +export const CommandExecutionOutput = memo( + ({ item: { command, state, id } }) => { + const dispatch = useConsoleStateDispatch(); + const RenderComponent = command.commandDefinition.RenderComponent; - try { - const commandOutput = await commandService.executeCommand(command); - setOutput(commandOutput.result); + const isRunning = useMemo(() => { + return state.status === 'pending'; + }, [state.status]); - // FIXME: PT the console should scroll the bottom as well - } catch (error) { - setOutput(); - } + /** Updates the Command's status */ + const setCommandStatus = useCallback( + (status: CommandExecutionState['status']) => { + dispatch({ + type: 'updateCommandStatusState', + payload: { + id, + value: status, + }, + }); + }, + [dispatch, id] + ); - clearTimeout(timeoutId); - setIsRunning(false); - setShowRunInTheBackground(false); - })(); - }, [command, commandService]); + /** Updates the Command's execution store */ + const setCommandStore = useCallback( + (store) => { + dispatch({ + type: 'updateCommandStoreState', + payload: { + id, + value: store, + }, + }); + }, + [dispatch, id] + ); - useEffect(() => { - if (!isRunning) { - dispatch({ type: 'scrollDown' }); - } - }, [isRunning, dispatch]); - - return ( - - {showRunInBackground && ( -
- - - + return ( + +
+ + {isRunning && } +
+
+
- )} -
- - {isRunning && ( - <> - - - )} -
-
{output}
-
- ); -}); + + ); + } +); CommandExecutionOutput.displayName = 'CommandExecutionOutput'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx index 9d17d83f0266f..68b2aab558d83 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/command_usage.tsx @@ -79,22 +79,20 @@ export const CommandUsage = memo(({ commandDef }) => { {hasArgs && ( <> -

- - - {commandDef.mustHaveArgs && commandDef.args && hasArgs && ( - - - - )} - -

+ + + {commandDef.mustHaveArgs && commandDef.args && hasArgs && ( + + + + )} + {commandDef.args && ( { it("should persist a console's command output history on hide/show", async () => { await render(); enterConsoleCommand(renderResult, 'help', { dataTestSubj: 'testRunningConsole' }); - enterConsoleCommand(renderResult, 'help', { dataTestSubj: 'testRunningConsole' }); + enterConsoleCommand(renderResult, 'cmd1', { dataTestSubj: 'testRunningConsole' }); await waitFor(() => { expect(renderResult.queryAllByTestId('testRunningConsole-historyItem')).toHaveLength(2); }); + // Hide the console userEvent.click(renderResult.getByTestId('consolePopupHideButton')); await waitFor(() => { expect( @@ -317,6 +318,7 @@ describe('When using ConsoleManager', () => { ).toBe(true); }); + // Open the console back up and ensure prior items still there await openRunningConsole(); await waitFor(() => { @@ -324,6 +326,46 @@ describe('When using ConsoleManager', () => { }); }); + it('should provide console rendering state between show/hide', async () => { + const expectedStoreValue = JSON.stringify({ foo: 'bar' }, null, 2); + await render(); + enterConsoleCommand(renderResult, 'cmd1', { dataTestSubj: 'testRunningConsole' }); + + // Command should have `pending` status and no store values + expect(renderResult.getByTestId('exec-output-statusState').textContent).toEqual( + 'status: pending' + ); + expect(renderResult.getByTestId('exec-output-storeStateJson').textContent).toEqual('{}'); + + // Wait for component to update the status and store values + await waitFor(() => { + expect(renderResult.getByTestId('exec-output-statusState').textContent).toMatch( + 'status: success' + ); + }); + expect(renderResult.getByTestId('exec-output-storeStateJson').textContent).toEqual( + expectedStoreValue + ); + + // Hide the console + userEvent.click(renderResult.getByTestId('consolePopupHideButton')); + await waitFor(() => { + expect( + renderResult.getByTestId('consolePopupWrapper').classList.contains('is-hidden') + ).toBe(true); + }); + + // Open the console back up and ensure `status` and `store` are the last set of values + await openRunningConsole(); + + expect(renderResult.getByTestId('exec-output-statusState').textContent).toMatch( + 'status: success' + ); + expect(renderResult.getByTestId('exec-output-storeStateJson').textContent).toEqual( + expectedStoreValue + ); + }); + describe('and the terminate confirmation is shown', () => { const clickOnTerminateButton = async () => { userEvent.click(renderResult.getByTestId('consolePopupTerminateButton')); diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/mocks.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/mocks.tsx index 57ec4246caf41..0b841f4118d1f 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/mocks.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_manager/mocks.tsx @@ -9,7 +9,7 @@ import React, { memo, useCallback } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { ConsoleRegistrationInterface, RegisteredConsoleClient } from './types'; import { useConsoleManager } from './console_manager'; -import { getCommandServiceMock } from '../../mocks'; +import { getCommandListMock } from '../../mocks'; export const getNewConsoleRegistrationMock = ( overrides: Partial = {} @@ -20,7 +20,7 @@ export const getNewConsoleRegistrationMock = ( meta: { about: 'for unit testing ' }, consoleProps: { 'data-test-subj': 'testRunningConsole', - commandService: getCommandServiceMock(), + commands: getCommandListMock(), }, onBeforeTerminate: jest.fn(), ...overrides, diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/console_state.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/console_state.tsx index 852b2b1ab58fe..66c874e4e27a8 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/console_state.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/console_state.tsx @@ -17,10 +17,10 @@ type ConsoleStateProviderProps = PropsWithChildren<{}> & InitialStateInterface; * A Console wide data store for internal state management between inner components */ export const ConsoleStateProvider = memo( - ({ commandService, scrollToBottom, dataTestSubj, children }) => { + ({ commands, scrollToBottom, HelpComponent, dataTestSubj, children }) => { const [state, dispatch] = useReducer( stateDataReducer, - { commandService, scrollToBottom, dataTestSubj }, + { commands, scrollToBottom, HelpComponent, dataTestSubj }, initiateState ); diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_reducer.ts b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_reducer.ts index 94175d9821ae7..68024aa5b7cfc 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_reducer.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_reducer.ts @@ -5,26 +5,28 @@ * 2.0. */ -import { ConsoleDataState, ConsoleStoreReducer } from './types'; +import { handleUpdateCommandState } from './state_update_handlers/handle_update_command_state'; +import type { ConsoleDataState, ConsoleStoreReducer } from './types'; import { handleExecuteCommand } from './state_update_handlers/handle_execute_command'; -import { ConsoleBuiltinCommandsService } from '../../service/builtin_command_service'; +import { getBuiltinCommands } from '../../service/builtin_commands'; export type InitialStateInterface = Pick< ConsoleDataState, - 'commandService' | 'scrollToBottom' | 'dataTestSubj' + 'commands' | 'scrollToBottom' | 'dataTestSubj' | 'HelpComponent' >; export const initiateState = ({ - commandService, + commands, scrollToBottom, dataTestSubj, + HelpComponent, }: InitialStateInterface): ConsoleDataState => { return { - commandService, + commands: getBuiltinCommands().concat(commands), scrollToBottom, + HelpComponent, dataTestSubj, commandHistory: [], - builtinCommandService: new ConsoleBuiltinCommandsService(), }; }; @@ -36,6 +38,13 @@ export const stateDataReducer: ConsoleStoreReducer = (state, action) => { case 'executeCommand': return handleExecuteCommand(state, action); + + case 'updateCommandStatusState': + case 'updateCommandStoreState': + return handleUpdateCommandState(state, action); + + case 'clear': + return { ...state, commandHistory: [] }; } return state; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.test.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.test.tsx index 06ecc344d5596..d19376395742f 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.test.tsx @@ -15,13 +15,13 @@ import { ConsoleProps } from '../../../types'; describe('When a Console command is entered by the user', () => { let render: (props?: Partial) => ReturnType; let renderResult: ReturnType; - let commandServiceMock: ConsoleTestSetup['commandServiceMock']; + let commands: ConsoleTestSetup['commands']; let enterCommand: ConsoleTestSetup['enterCommand']; beforeEach(() => { const testSetup = getConsoleTestSetup(); - ({ commandServiceMock, enterCommand } = testSetup); + ({ commands, enterCommand } = testSetup); render = (props = {}) => (renderResult = testSetup.renderConsole(props)); }); @@ -34,18 +34,16 @@ describe('When a Console command is entered by the user', () => { await waitFor(() => { expect(renderResult.getAllByTestId('test-commandList-command')).toHaveLength( // `+2` to account for builtin commands - commandServiceMock.getCommandList().length + 2 + commands.length + 2 ); }); }); it('should display custom help output when Command service has `getHelp()` defined', async () => { - commandServiceMock.getHelp = async () => { - return { - result:
{'help output'}
, - }; + const HelpComponent: React.FunctionComponent = () => { + return
{'help output'}
; }; - render(); + render({ HelpComponent }); enterCommand('help'); await waitFor(() => { @@ -73,11 +71,15 @@ describe('When a Console command is entered by the user', () => { }); it('should should custom command `--help` output when Command service defines `getCommandUsage()`', async () => { - commandServiceMock.getCommandUsage = async () => { - return { - result:
{'command help here'}
, + const cmd2 = commands.find((command) => command.name === 'cmd2'); + + if (cmd2) { + cmd2.HelpComponent = () => { + return
{'command help here'}
; }; - }; + cmd2.HelpComponent.displayName = 'HelpComponent'; + } + render(); enterCommand('cmd2 --help'); diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx index 2815ec4605917..c387cf3d90a8f 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_execute_command.tsx @@ -5,19 +5,19 @@ * 2.0. */ -/* eslint complexity: ["error", 40]*/ -// FIXME:PT remove the complexity - -import React from 'react'; import { i18n } from '@kbn/i18n'; -import { ConsoleDataAction, ConsoleDataState, ConsoleStoreReducer } from '../types'; +import { v4 as uuidV4 } from 'uuid'; +import { HelpCommandArgument } from '../../builtin_commands/help_command_argument'; +import { + CommandHistoryItem, + ConsoleDataAction, + ConsoleDataState, + ConsoleStoreReducer, +} from '../types'; import { parseCommandInput } from '../../../service/parsed_command_input'; -import { HistoryItem } from '../../history_item'; import { UnknownCommand } from '../../unknow_comand'; -import { HelpOutput } from '../../help_output'; import { BadArgument } from '../../bad_argument'; -import { CommandExecutionOutput } from '../../command_execution_output'; -import { CommandDefinition } from '../../../types'; +import { Command, CommandDefinition, CommandExecutionComponentProps } from '../../../types'; const toCliArgumentOption = (argName: string) => `--${argName}`; @@ -41,6 +41,36 @@ const updateStateWithNewCommandHistoryItem = ( }; }; +const UnknownCommandDefinition: CommandDefinition = { + name: 'unknown-command', + about: 'unknown command', + RenderComponent: () => null, +}; + +const createCommandExecutionState = ( + store: CommandExecutionComponentProps['store'] = {} +): CommandHistoryItem['state'] => { + return { + status: 'pending', + store, + }; +}; + +const cloneCommandDefinitionWithNewRenderComponent = ( + command: Command, + RenderComponent: CommandDefinition['RenderComponent'] +): Command => { + return { + ...command, + commandDefinition: { + ...command.commandDefinition, + // We use the original command definition, but replace + // the RenderComponent for this invocation + RenderComponent, + }, + }; +}; + export const handleExecuteCommand: ConsoleStoreReducer< ConsoleDataAction & { type: 'executeCommand' } > = (state, action) => { @@ -50,116 +80,98 @@ export const handleExecuteCommand: ConsoleStoreReducer< return state; } - const { commandService, builtinCommandService } = state; - - // Is it an internal command? - if (builtinCommandService.isBuiltin(parsedInput.name)) { - const commandOutput = builtinCommandService.executeBuiltinCommand(parsedInput, commandService); - - if (commandOutput.clearBuffer) { - return { - ...state, - commandHistory: [], - }; - } - - return updateStateWithNewCommandHistoryItem(state, commandOutput.result); - } - - // ---------------------------------------------------- - // Validate and execute the user defined command - // ---------------------------------------------------- - const commandDefinition = commandService - .getCommandList() - .find((definition) => definition.name === parsedInput.name); + const { commands } = state; + const commandDefinition: CommandDefinition | undefined = commands.find( + (definition) => definition.name === parsedInput.name + ); // Unknown command if (!commandDefinition) { - return updateStateWithNewCommandHistoryItem( - state, - - - - ); + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: { + input: parsedInput.input, + args: parsedInput, + commandDefinition: { + ...UnknownCommandDefinition, + RenderComponent: UnknownCommand, + }, + }, + state: createCommandExecutionState(), + }); } + const command = { + input: parsedInput.input, + args: parsedInput, + commandDefinition, + }; const requiredArgs = getRequiredArguments(commandDefinition.args); // If args were entered, then validate them if (parsedInput.hasArgs()) { // Show command help if (parsedInput.hasArg('help')) { - return updateStateWithNewCommandHistoryItem( - state, - - - {(commandService.getCommandUsage || builtinCommandService.getCommandUsage)( - commandDefinition - )} - - - ); + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, HelpCommandArgument), + state: createCommandExecutionState(), + }); } // Command supports no arguments if (!commandDefinition.args || Object.keys(commandDefinition.args).length === 0) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate( - 'xpack.securitySolution.console.commandValidation.noArgumentsSupported', - { - defaultMessage: 'command does not support any arguments', - } - )} - - - ); + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.noArgumentsSupported', + { + defaultMessage: 'command does not support any arguments', + } + ), + }), + }); } // no unknown arguments allowed? if (parsedInput.unknownArgs && parsedInput.unknownArgs.length) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate('xpack.securitySolution.console.commandValidation.unknownArgument', { + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.unknownArgument', + { defaultMessage: 'unknown argument(s): {unknownArgs}', values: { unknownArgs: parsedInput.unknownArgs.join(', '), }, - })} - - - ); + } + ), + }), + }); } // Missing required Arguments for (const requiredArg of requiredArgs) { if (!parsedInput.args[requiredArg]) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate( - 'xpack.securitySolution.console.commandValidation.missingRequiredArg', - { - defaultMessage: 'missing required argument: {argName}', - values: { - argName: toCliArgumentOption(requiredArg), - }, - } - )} - - - ); + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.missingRequiredArg', + { + defaultMessage: 'missing required argument: {argName}', + values: { + argName: toCliArgumentOption(requiredArg), + }, + } + ), + }), + }); } } @@ -170,17 +182,19 @@ export const handleExecuteCommand: ConsoleStoreReducer< // Unknown argument if (!argDefinition) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate('xpack.securitySolution.console.commandValidation.unsupportedArg', { + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.unsupportedArg', + { defaultMessage: 'unsupported argument: {argName}', values: { argName: toCliArgumentOption(argName) }, - })} - - - ); + } + ), + }), + }); } // does not allow multiple values @@ -189,81 +203,76 @@ export const handleExecuteCommand: ConsoleStoreReducer< Array.isArray(argInput.values) && argInput.values.length > 0 ) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate( - 'xpack.securitySolution.console.commandValidation.argSupportedOnlyOnce', - { - defaultMessage: 'argument can only be used once: {argName}', - values: { argName: toCliArgumentOption(argName) }, - } - )} - - - ); + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.argSupportedOnlyOnce', + { + defaultMessage: 'argument can only be used once: {argName}', + values: { argName: toCliArgumentOption(argName) }, + } + ), + }), + }); } if (argDefinition.validate) { const validationResult = argDefinition.validate(argInput); if (validationResult !== true) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate( - 'xpack.securitySolution.console.commandValidation.invalidArgValue', - { - defaultMessage: 'invalid argument value: {argName}. {error}', - values: { argName: toCliArgumentOption(argName), error: validationResult }, - } - )} - - - ); + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.invalidArgValue', + { + defaultMessage: 'invalid argument value: {argName}. {error}', + values: { argName: toCliArgumentOption(argName), error: validationResult }, + } + ), + }), + }); } } } } else if (requiredArgs.length > 0) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate('xpack.securitySolution.console.commandValidation.mustHaveArgs', { + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.mustHaveArgs', + { defaultMessage: 'missing required arguments: {requiredArgs}', values: { requiredArgs: requiredArgs.map((argName) => toCliArgumentOption(argName)).join(', '), }, - })} - - - ); + } + ), + }), + }); } else if (commandDefinition.mustHaveArgs) { - return updateStateWithNewCommandHistoryItem( - state, - - - {i18n.translate('xpack.securitySolution.console.commandValidation.oneArgIsRequired', { + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command: cloneCommandDefinitionWithNewRenderComponent(command, BadArgument), + state: createCommandExecutionState({ + errorMessage: i18n.translate( + 'xpack.securitySolution.console.commandValidation.oneArgIsRequired', + { defaultMessage: 'at least one argument must be used', - })} - - - ); + } + ), + }), + }); } // All is good. Execute the command - return updateStateWithNewCommandHistoryItem( - state, - - - - ); + return updateStateWithNewCommandHistoryItem(state, { + id: uuidV4(), + command, + state: createCommandExecutionState(), + }); }; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_update_command_state.ts b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_update_command_state.ts new file mode 100644 index 0000000000000..8e176019990a3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/state_update_handlers/handle_update_command_state.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + CommandExecutionState, + CommandHistoryItem, + ConsoleDataAction, + ConsoleStoreReducer, +} from '../types'; + +type UpdateCommandStateAction = ConsoleDataAction & { + type: 'updateCommandStoreState' | 'updateCommandStatusState'; +}; + +export const handleUpdateCommandState: ConsoleStoreReducer = ( + state, + { type, payload: { id, value } } +) => { + let foundIt = false; + const updatedCommandHistory = state.commandHistory.map((item) => { + if (foundIt || item.id !== id) { + return item; + } + + foundIt = true; + + const updatedCommandState: CommandHistoryItem = { + ...item, + state: { + ...item.state, + }, + }; + + switch (type) { + case 'updateCommandStoreState': + updatedCommandState.state.store = value as CommandExecutionState['store']; + break; + case 'updateCommandStatusState': + // If the status was not changed, then there is nothing to be done here, so + // instead of triggering a state change (and UI re-render), just return the + // original item; + if (updatedCommandState.state.status === value) { + foundIt = false; + return item; + } + + updatedCommandState.state.status = value as CommandExecutionState['status']; + break; + } + + return updatedCommandState; + }); + + if (foundIt) { + return { + ...state, + commandHistory: updatedCommandHistory, + }; + } + + return state; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/types.ts b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/types.ts index 72810d31e3248..356033e147c56 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/console_state/types.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/components/console_state/types.ts @@ -5,28 +5,51 @@ * 2.0. */ -import { Dispatch, Reducer } from 'react'; -import { CommandServiceInterface } from '../../types'; -import { HistoryItemComponent } from '../history_item'; -import { BuiltinCommandServiceInterface } from '../../service/types.builtin_command_service'; +import type { Dispatch, Reducer } from 'react'; +import type { Command, CommandDefinition, CommandExecutionComponent } from '../../types'; export interface ConsoleDataState { - /** Command service defined on input to the `Console` component by consumers of the component */ - commandService: CommandServiceInterface; - /** Command service for builtin console commands */ - builtinCommandService: BuiltinCommandServiceInterface; + /** + * Commands available in the console, which includes both the builtin command and the ones + * defined on input to the `Console` component by consumers of the component + */ + commands: CommandDefinition[]; + /** UI function that scrolls the console down to the bottom */ scrollToBottom: () => void; + /** * List of commands entered by the user and being shown in the UI */ - commandHistory: Array>; + commandHistory: CommandHistoryItem[]; + /** Component defined on input to the Console that will handle the `help` command */ + HelpComponent?: CommandExecutionComponent; dataTestSubj?: string; } +export interface CommandHistoryItem { + id: string; + command: Command; + state: CommandExecutionState; +} + +export interface CommandExecutionState { + status: 'pending' | 'success' | 'error'; + store: Record; +} + export type ConsoleDataAction = | { type: 'scrollDown' } - | { type: 'executeCommand'; payload: { input: string } }; + | { type: 'executeCommand'; payload: { input: string } } + | { type: 'clear' } + | { + type: 'updateCommandStoreState'; + payload: { id: string; value: CommandExecutionState['store'] }; + } + | { + type: 'updateCommandStatusState'; + payload: { id: string; value: CommandExecutionState['status'] }; + }; export interface ConsoleStore { state: ConsoleDataState; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/help_output.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/help_output.tsx index b0a2217e169c4..597d979e00034 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/help_output.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/help_output.tsx @@ -5,55 +5,30 @@ * 2.0. */ -import React, { memo, ReactNode, useEffect, useState } from 'react'; -import { EuiCallOut, EuiCallOutProps, EuiLoadingChart } from '@elastic/eui'; -import { UserCommandInput } from './user_command_input'; -import { CommandExecutionFailure } from './command_execution_failure'; +import React, { memo, PropsWithChildren, ReactNode } from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { MaybeImmutable } from '../../../../../common/endpoint/types'; +import { Command } from '..'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj'; -export interface HelpOutputProps extends Pick { - input: string; - children: ReactNode | Promise<{ result: ReactNode }>; -} -export const HelpOutput = memo(({ input, children, ...euiCalloutProps }) => { - const [content, setContent] = useState(); +type HelpOutputProps = PropsWithChildren<{ + command: MaybeImmutable; + title?: ReactNode; +}>; +export const HelpOutput = memo(({ title, children }) => { const getTestId = useTestIdGenerator(useDataTestSubj()); - useEffect(() => { - if (children instanceof Promise) { - (async () => { - try { - const response = await (children as Promise<{ - result: ReactNode; - }>); - setContent(response.result); - } catch (error) { - setContent(); - } - })(); - - return; - } - - setContent(children); - }, [children]); - return ( -
-
- -
- - {content} - -
+ + {children} + ); }); HelpOutput.displayName = 'HelpOutput'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/history_output.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/history_output.tsx index 088a6fac57ae4..cd03f9d39a39d 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/history_output.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/history_output.tsx @@ -5,12 +5,14 @@ * 2.0. */ -import React, { memo, useEffect } from 'react'; +import React, { memo, useEffect, useMemo } from 'react'; import { CommonProps, EuiFlexGroup } from '@elastic/eui'; +import { CommandExecutionOutput } from './command_execution_output'; import { useCommandHistory } from '../hooks/state_selectors/use_command_history'; import { useConsoleStateDispatch } from '../hooks/state_selectors/use_console_state_dispatch'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj'; +import { HistoryItem } from './history_item'; export type OutputHistoryProps = CommonProps; @@ -19,6 +21,16 @@ export const HistoryOutput = memo((commonProps) => { const dispatch = useConsoleStateDispatch(); const getTestId = useTestIdGenerator(useDataTestSubj()); + const historyBody = useMemo(() => { + return historyItems.map((historyItem) => { + return ( + + + + ); + }); + }, [historyItems]); + // Anytime we add a new item to the history // scroll down so that command input remains visible useEffect(() => { @@ -34,7 +46,7 @@ export const HistoryOutput = memo((commonProps) => { alignItems="flexEnd" responsive={false} > - {historyItems} + {historyBody} ); }); diff --git a/x-pack/plugins/security_solution/public/management/components/console/components/unknow_comand.tsx b/x-pack/plugins/security_solution/public/management/components/console/components/unknow_comand.tsx index 5529457cbb05a..8397c7727de81 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/components/unknow_comand.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/components/unknow_comand.tsx @@ -5,42 +5,38 @@ * 2.0. */ -import React, { memo } from 'react'; +import React, { memo, useEffect } from 'react'; import { EuiCallOut, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { UserCommandInput } from './user_command_input'; +import { CommandExecutionComponentProps } from '../types'; import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj'; import { useTestIdGenerator } from '../../hooks/use_test_id_generator'; -export interface UnknownCommand { - input: string; -} -export const UnknownCommand = memo(({ input }) => { +export const UnknownCommand = memo(({ setStatus }) => { const getTestId = useTestIdGenerator(useDataTestSubj()); + useEffect(() => { + setStatus('success'); + }, [setStatus]); + return ( - <> -
- -
- - - - - - {'help'}, - }} - /> - - - + + + + + + {'help'}, + }} + /> + + ); }); UnknownCommand.displayName = 'UnknownCommand'; diff --git a/x-pack/plugins/security_solution/public/management/components/console/console.tsx b/x-pack/plugins/security_solution/public/management/components/console/console.tsx index 0f3645037df02..874dbc2eabae0 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/console.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/console.tsx @@ -45,7 +45,7 @@ const ConsoleWindow = styled.div` `; export const Console = memo( - ({ prompt, commandService, managedKey, ...commonProps }) => { + ({ prompt, commands, HelpComponent, managedKey, ...commonProps }) => { const consoleWindowRef = useRef(null); const inputFocusRef: CommandInputProps['focusRef'] = useRef(null); const getTestId = useTestIdGenerator(commonProps['data-test-subj']); @@ -72,8 +72,9 @@ export const Console = memo( return ( {/* diff --git a/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_builtin_command_service.ts b/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_with_command_list.ts similarity index 58% rename from x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_builtin_command_service.ts rename to x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_with_command_list.ts index 22167d5066743..4f63c55a36098 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_builtin_command_service.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_with_command_list.ts @@ -6,8 +6,11 @@ */ import { useConsoleStore } from '../../components/console_state/console_state'; -import { CommandServiceInterface } from '../../types'; +import type { CommandDefinition } from '../../types'; -export const useCommandService = (): CommandServiceInterface => { - return useConsoleStore().state.builtinCommandService; +/** + * Returns the Command service that the console was provided on input + */ +export const useWithCommandList = (): CommandDefinition[] => { + return useConsoleStore().state.commands; }; diff --git a/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_command_service.ts b/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_with_custom_help_component.ts similarity index 62% rename from x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_command_service.ts rename to x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_with_custom_help_component.ts index 66ce0c2b5eb43..b90e5166c81d7 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_command_service.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/hooks/state_selectors/use_with_custom_help_component.ts @@ -6,8 +6,8 @@ */ import { useConsoleStore } from '../../components/console_state/console_state'; -import { CommandServiceInterface } from '../../types'; +import { ConsoleDataState } from '../../components/console_state/types'; -export const useCommandService = (): CommandServiceInterface => { - return useConsoleStore().state.commandService; +export const useWithCustomHelpComponent = (): ConsoleDataState['HelpComponent'] => { + return useConsoleStore().state.HelpComponent; }; diff --git a/x-pack/plugins/security_solution/public/management/components/console/index.ts b/x-pack/plugins/security_solution/public/management/components/console/index.ts index 4264aa5a8f830..1603d4b15f353 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/index.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/index.ts @@ -7,7 +7,7 @@ export { Console } from './console'; export { ConsoleManager, useConsoleManager } from './components/console_manager'; -export type { CommandServiceInterface, CommandDefinition, Command, ConsoleProps } from './types'; +export type { CommandDefinition, Command, ConsoleProps } from './types'; export type { ConsoleRegistrationInterface, RegisteredConsoleClient, diff --git a/x-pack/plugins/security_solution/public/management/components/console/mocks.tsx b/x-pack/plugins/security_solution/public/management/components/console/mocks.tsx index d89c5f5374d47..ea24a174498dd 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/mocks.tsx +++ b/x-pack/plugins/security_solution/public/management/components/console/mocks.tsx @@ -7,20 +7,19 @@ /* eslint-disable import/no-extraneous-dependencies */ -import React from 'react'; +import React, { useEffect } from 'react'; import { EuiCode } from '@elastic/eui'; import userEvent from '@testing-library/user-event'; import { act } from '@testing-library/react'; import { Console } from './console'; -import type { Command, CommandServiceInterface, ConsoleProps } from './types'; +import type { ConsoleProps, CommandDefinition, CommandExecutionComponent } from './types'; import type { AppContextTestRender } from '../../../common/mock/endpoint'; import { createAppRootMockRenderer } from '../../../common/mock/endpoint'; -import { CommandDefinition } from './types'; export interface ConsoleTestSetup { renderConsole(props?: Partial): ReturnType; - commandServiceMock: jest.Mocked; + commands: CommandDefinition[]; enterCommand( cmd: string, @@ -74,25 +73,16 @@ export const getConsoleTestSetup = (): ConsoleTestSetup => { let renderResult: ReturnType; - const commandServiceMock = getCommandServiceMock(); + const commandList = getCommandListMock(); const renderConsole: ConsoleTestSetup['renderConsole'] = ({ prompt = '$$>', - commandService = commandServiceMock, + commands = commandList, 'data-test-subj': dataTestSubj = 'test', ...others } = {}) => { - if (commandService !== commandServiceMock) { - throw new Error('Must use CommandService provided by test setup'); - } - return (renderResult = mockedContext.render( - + )); }; @@ -102,91 +92,107 @@ export const getConsoleTestSetup = (): ConsoleTestSetup => { return { renderConsole, - commandServiceMock, + commands: commandList, enterCommand, }; }; -export const getCommandServiceMock = (): jest.Mocked => { - return { - getCommandList: jest.fn(() => { - const commands: CommandDefinition[] = [ - { - name: 'cmd1', - about: 'a command with no options', - }, - { - name: 'cmd2', - about: 'runs cmd 2', - args: { - file: { - about: 'Includes file in the run', - required: true, - allowMultiples: false, - validate: () => { - return true; - }, - }, - ext: { - about: 'optional argument', - required: false, - allowMultiples: false, - }, - bad: { - about: 'will fail validation', - required: false, - allowMultiples: false, - validate: () => 'This is a bad value', - }, +export const getCommandListMock = (): CommandDefinition[] => { + const RenderComponent: CommandExecutionComponent = ({ + command, + status, + setStatus, + setStore, + store, + }) => { + useEffect(() => { + if (status !== 'success') { + new Promise((r) => setTimeout(r, 500)).then(() => { + setStatus('success'); + setStore({ foo: 'bar' }); + }); + } + }, [setStatus, setStore, status]); + + return ( +
+
{`${command.commandDefinition.name}`}
+
{`command input: ${command.input}`}
+ + {JSON.stringify(command.args, null, 2)} + +
{'Command render state:'}
+
{`status: ${status}`}
+ + {JSON.stringify(store, null, 2)} + +
+ ); + }; + + const commands: CommandDefinition[] = [ + { + name: 'cmd1', + about: 'a command with no options', + RenderComponent: jest.fn(RenderComponent), + }, + { + name: 'cmd2', + about: 'runs cmd 2', + RenderComponent: jest.fn(RenderComponent), + args: { + file: { + about: 'Includes file in the run', + required: true, + allowMultiples: false, + validate: () => { + return true; }, }, - { - name: 'cmd3', - about: 'allows argument to be used multiple times', - args: { - foo: { - about: 'foo stuff', - required: true, - allowMultiples: true, - }, - }, + ext: { + about: 'optional argument', + required: false, + allowMultiples: false, }, - { - name: 'cmd4', - about: 'all options optinal, but at least one is required', - mustHaveArgs: true, - args: { - foo: { - about: 'foo stuff', - required: false, - allowMultiples: true, - }, - bar: { - about: 'bar stuff', - required: false, - allowMultiples: true, - }, - }, + bad: { + about: 'will fail validation', + required: false, + allowMultiples: false, + validate: () => 'This is a bad value', }, - ]; - - return commands; - }), - - executeCommand: jest.fn(async (command: Command) => { - await new Promise((r) => setTimeout(r, 1)); - - return { - result: ( -
-
{`${command.commandDefinition.name}`}
-
{`command input: ${command.input}`}
- - {JSON.stringify(command.args, null, 2)} - -
- ), - }; - }), - }; + }, + }, + { + name: 'cmd3', + about: 'allows argument to be used multiple times', + RenderComponent: jest.fn(RenderComponent), + args: { + foo: { + about: 'foo stuff', + required: true, + allowMultiples: true, + }, + }, + }, + { + name: 'cmd4', + about: 'all options optional, but at least one is required', + RenderComponent: jest.fn(RenderComponent), + mustHaveArgs: true, + args: { + foo: { + about: 'foo stuff', + required: false, + allowMultiples: true, + }, + bar: { + about: 'bar stuff', + required: false, + allowMultiples: true, + }, + }, + }, + ]; + + return commands; }; diff --git a/x-pack/plugins/security_solution/public/management/components/console/service/builtin_command_service.tsx b/x-pack/plugins/security_solution/public/management/components/console/service/builtin_command_service.tsx deleted file mode 100644 index 6cd8af0dc6eff..0000000000000 --- a/x-pack/plugins/security_solution/public/management/components/console/service/builtin_command_service.tsx +++ /dev/null @@ -1,102 +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, { ReactNode } from 'react'; -import { i18n } from '@kbn/i18n'; -import { HistoryItem, HistoryItemComponent } from '../components/history_item'; -import { HelpOutput } from '../components/help_output'; -import { ParsedCommandInput } from './parsed_command_input'; -import { CommandList } from '../components/command_list'; -import { CommandUsage } from '../components/command_usage'; -import { Command, CommandDefinition, CommandServiceInterface } from '../types'; -import { BuiltinCommandServiceInterface } from './types.builtin_command_service'; - -const builtInCommands = (): CommandDefinition[] => { - return [ - { - name: 'help', - about: i18n.translate('xpack.securitySolution.console.builtInCommands.helpAbout', { - defaultMessage: 'View list of available commands', - }), - }, - { - name: 'clear', - about: i18n.translate('xpack.securitySolution.console.builtInCommands.clearAbout', { - defaultMessage: 'Clear the console buffer', - }), - }, - ]; -}; - -export class ConsoleBuiltinCommandsService implements BuiltinCommandServiceInterface { - constructor(private commandList = builtInCommands()) {} - - getCommandList(): CommandDefinition[] { - return this.commandList; - } - - async executeCommand(command: Command): Promise<{ result: ReactNode }> { - return { - result: null, - }; - } - - executeBuiltinCommand( - parsedInput: ParsedCommandInput, - contextConsoleService: CommandServiceInterface - ): { result: ReturnType | null; clearBuffer?: boolean } { - switch (parsedInput.name) { - case 'help': - return { - result: ( - - - {this.getHelpContent(parsedInput, contextConsoleService)} - - - ), - }; - - case 'clear': - return { - result: null, - clearBuffer: true, - }; - } - - return { result: null }; - } - - async getHelpContent( - parsedInput: ParsedCommandInput, - commandService: CommandServiceInterface - ): Promise<{ result: ReactNode }> { - let helpOutput: ReactNode; - - if (commandService.getHelp) { - helpOutput = (await commandService.getHelp()).result; - } else { - helpOutput = ( - - ); - } - - return { - result: helpOutput, - }; - } - - isBuiltin(name: string): boolean { - return !!this.commandList.find((command) => command.name === name); - } - - async getCommandUsage(command: CommandDefinition): Promise<{ result: ReactNode }> { - return { - result: , - }; - } -} diff --git a/x-pack/plugins/security_solution/public/management/components/console/service/builtin_commands.tsx b/x-pack/plugins/security_solution/public/management/components/console/service/builtin_commands.tsx new file mode 100644 index 0000000000000..5869e3b4472cb --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/console/service/builtin_commands.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { ClearCommand } from '../components/builtin_commands/clear_command'; +import { HelpCommand } from '../components/builtin_commands/help_command'; +import { CommandDefinition } from '../types'; + +export const getBuiltinCommands = (): CommandDefinition[] => { + return [ + { + name: 'help', + about: i18n.translate('xpack.securitySolution.console.builtInCommands.helpAbout', { + defaultMessage: 'View list of available commands', + }), + RenderComponent: HelpCommand, + }, + { + name: 'clear', + about: i18n.translate('xpack.securitySolution.console.builtInCommands.clearAbout', { + defaultMessage: 'Clear the console buffer', + }), + RenderComponent: ClearCommand, + }, + ]; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/console/service/types.builtin_command_service.ts b/x-pack/plugins/security_solution/public/management/components/console/service/types.builtin_command_service.ts deleted file mode 100644 index dbd5347ea99c2..0000000000000 --- a/x-pack/plugins/security_solution/public/management/components/console/service/types.builtin_command_service.ts +++ /dev/null @@ -1,27 +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 { ReactNode } from 'react'; -import { CommandDefinition, CommandServiceInterface } from '../types'; -import { ParsedCommandInput } from './parsed_command_input'; -import { HistoryItemComponent } from '../components/history_item'; - -export interface BuiltinCommandServiceInterface extends CommandServiceInterface { - executeBuiltinCommand( - parsedInput: ParsedCommandInput, - contextConsoleService: CommandServiceInterface - ): { result: ReturnType | null; clearBuffer?: boolean }; - - getHelpContent( - parsedInput: ParsedCommandInput, - commandService: CommandServiceInterface - ): Promise<{ result: ReactNode }>; - - isBuiltin(name: string): boolean; - - getCommandUsage(command: CommandDefinition): Promise<{ result: ReactNode }>; -} diff --git a/x-pack/plugins/security_solution/public/management/components/console/types.ts b/x-pack/plugins/security_solution/public/management/components/console/types.ts index 6b15f03988313..fec4b2722cc92 100644 --- a/x-pack/plugins/security_solution/public/management/components/console/types.ts +++ b/x-pack/plugins/security_solution/public/management/components/console/types.ts @@ -5,16 +5,34 @@ * 2.0. */ -import { ReactNode } from 'react'; -import { CommonProps } from '@elastic/eui'; -import { ParsedArgData, ParsedCommandInput } from './service/parsed_command_input'; +import type { ComponentType, ComponentProps } from 'react'; +import type { CommonProps } from '@elastic/eui'; +import type { CommandExecutionState } from './components/console_state/types'; +import type { Immutable } from '../../../../common/endpoint/types'; +import type { ParsedArgData, ParsedCommandInput } from './service/parsed_command_input'; export interface CommandDefinition { name: string; about: string; - validator?: () => Promise; + /** + * The Component that will be used to render the Command + */ + RenderComponent: CommandExecutionComponent; + /** + * If defined, this command's use of `--help` will be displayed using this component instead of + * the console's built in output. + */ + HelpComponent?: CommandExecutionComponent; + /** + * A store for any data needed when the command is executed. + * The entire `CommandDefinition` is passed along to the component + * that will handle it, so this data will be available there + */ + meta?: Record; + /** If all args are optional, but at least one must be defined, set to true */ mustHaveArgs?: boolean; + /** The list of arguments supported by this command */ args?: { [longName: string]: { required: boolean; @@ -28,7 +46,7 @@ export interface CommandDefinition { // Selector: Idea is that the schema can plugin in a rich component for the // user to select something (ex. a file) // FIXME: implement selector - selector?: () => unknown; + selector?: ComponentType; }; }; } @@ -46,26 +64,44 @@ export interface Command { commandDefinition: CommandDefinition; } -export interface CommandServiceInterface { - getCommandList(): CommandDefinition[]; - - executeCommand(command: Command): Promise<{ result: ReactNode }>; - +/** + * The component that will handle the Command execution and display the result. + */ +export type CommandExecutionComponent = ComponentType<{ + command: Command; /** - * If defined, then the `help` builtin command will display this output instead of the default one - * which is generated out of the Command list + * A data store for the command execution to store data in, if needed. + * Because the Console could be closed/opened several times, which will cause this component + * to be `mounted`/`unmounted` several times, this data store will be beneficial for + * persisting data (ex. API response with IDs) that the command can use to determine + * if the command has already been executed or if it's a new instance. */ - getHelp?: () => Promise<{ result: ReactNode }>; - + store: Immutable; + /** Sets the `store` data above */ + setStore: (state: CommandExecutionState['store']) => void; /** - * If defined, then the output of this function will be used to display individual - * command help (`--help`) + * The status of the command execution. + * Note that the console's UI will show the command as "busy" while the status here is + * `pending`. Ensure that once the action processing completes, that this is set to + * either `success` or `error`. */ - getCommandUsage?: (command: CommandDefinition) => Promise<{ result: ReactNode }>; -} + status: CommandExecutionState['status']; + /** Set the status of the command execution */ + setStatus: (status: CommandExecutionState['status']) => void; +}>; + +export type CommandExecutionComponentProps = ComponentProps; export interface ConsoleProps extends CommonProps { - commandService: CommandServiceInterface; + /** + * The list of Commands that will be available in the console for the user to execute + */ + commands: CommandDefinition[]; + /** + * If defined, then the `help` builtin command will display this output instead of the default one + * which is generated out of the Command list. + */ + HelpComponent?: CommandExecutionComponent; prompt?: string; /** * For internal use only! diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_console.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_console.tsx deleted file mode 100644 index 28472e123380a..0000000000000 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_console.tsx +++ /dev/null @@ -1,25 +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, { memo, useMemo } from 'react'; -import { Console } from '../console'; -import { EndpointConsoleCommandService } from './endpoint_console_command_service'; -import type { HostMetadata } from '../../../../common/endpoint/types'; - -export interface EndpointConsoleProps { - endpoint: HostMetadata; -} - -export const EndpointConsole = memo((props) => { - const consoleService = useMemo(() => { - return new EndpointConsoleCommandService(); - }, []); - - return `} commandService={consoleService} />; -}); - -EndpointConsole.displayName = 'EndpointConsole'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_console_command_service.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_console_command_service.tsx deleted file mode 100644 index 5028879bc1a49..0000000000000 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_console_command_service.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { ReactNode } from 'react'; -import { CommandServiceInterface, CommandDefinition, Command } from '../console'; - -/** - * Endpoint specific Response Actions (commands) for use with Console. - */ -export class EndpointConsoleCommandService implements CommandServiceInterface { - getCommandList(): CommandDefinition[] { - return []; - } - - async executeCommand(command: Command): Promise<{ result: ReactNode }> { - return { result: <> }; - } -} diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx index a48d770460ec9..759cc37ed902b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/blocklist.tsx @@ -71,7 +71,7 @@ const BLOCKLIST_PAGE_LABELS: ArtifactListPageProps['labels'] = { @@ -87,7 +87,7 @@ const BLOCKLIST_PAGE_LABELS: ArtifactListPageProps['labels'] = { }), emptyStateInfo: i18n.translate('xpack.securitySolution.blocklist.emptyStateInfo', { defaultMessage: - 'The blocklist prevents selected applications from running on your hosts by extending the list of processes the Endpoint considers malicious.', + 'The blocklist prevents specified applications from running on your hosts, extending the list of processes that Endpoint Security considers malicious.', }), emptyStatePrimaryButtonLabel: i18n.translate( 'xpack.securitySolution.blocklist.emptyStatePrimaryButtonLabel', diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx index 889c5005ec193..24da8b3b86a35 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx @@ -8,7 +8,6 @@ import React, { memo, useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import { encode, RisonValue } from 'rison-node'; -import styled from 'styled-components'; import type { Query } from '@kbn/es-query'; import { TimeHistory } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; @@ -19,12 +18,6 @@ import { useEndpointSelector } from '../hooks'; import * as selectors from '../../store/selectors'; import { clone } from '../../models/index_pattern'; -const AdminQueryBar = styled.div` - .globalQueryBar { - padding: 0; - } -`; - export const AdminSearchBar = memo(() => { const history = useHistory(); const { admin_query: _, ...queryParams } = useEndpointSelector(selectors.uiQueryParams); @@ -57,7 +50,7 @@ export const AdminSearchBar = memo(() => { return (
{searchBarIndexPatterns && searchBarIndexPatterns.length > 0 && ( - +
{ showQueryBar={true} showQueryInput={true} /> - +
)}
); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/dev_console.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/dev_console.tsx index 6761a32c6fb65..46b2a96faa9c9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/dev_console.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/dev_console.tsx @@ -5,7 +5,15 @@ * 2.0. */ -import React, { memo, useCallback, useMemo } from 'react'; +import React, { + memo, + ReactElement, + ReactNode, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { EuiButton, EuiCode, @@ -15,12 +23,11 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { useIsMounted } from '../../../components/hooks/use_is_mounted'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { useUrlParams } from '../../../components/hooks/use_url_params'; import { - Command, CommandDefinition, - CommandServiceInterface, Console, RegisteredConsoleClient, useConsoleManager, @@ -28,69 +35,138 @@ import { const delay = async (ms: number = 4000) => new Promise((r) => setTimeout(r, ms)); -class DevCommandService implements CommandServiceInterface { - getCommandList(): CommandDefinition[] { - return [ - { - name: 'cmd1', - about: 'Runs cmd1', - }, - { - name: 'get-file', - about: 'retrieve a file from the endpoint', - args: { - file: { - required: true, - allowMultiples: false, - about: 'the file path for the file to be retrieved', - }, - }, +const getCommandList = (): CommandDefinition[] => { + return [ + { + name: 'cmd1', + about: 'Runs cmd1', + RenderComponent: ({ command, setStatus, store, setStore }) => { + const isMounted = useIsMounted(); + + const [apiResponse, setApiResponse] = useState(null); + const [uiResponse, setUiResponse] = useState(null); + + // Emulate a real action where: + // 1. an api request is done to create the action + // 2. wait for a response + // 3. account for component mount/unmount and prevent duplicate api calls + + useEffect(() => { + (async () => { + // Emulate an api call + if (!store.apiInflight) { + setStore({ + ...store, + apiInflight: true, + }); + + window.console.warn(`${Math.random()} ------> cmd1: doing async work`); + + await delay(6000); + setApiResponse(`API was called at: ${new Date().toLocaleString()}`); + } + })(); + }, [setStore, store]); + + useEffect(() => { + (async () => { + const doUiResponse = () => { + setUiResponse( + + {`${command.commandDefinition.name}`} + {`command input: ${command.input}`} + {'Arguments provided:'} + {JSON.stringify(command.args, null, 2)} + + ); + }; + + if (store.apiResponse) { + doUiResponse(); + } else { + await delay(); + doUiResponse(); + } + })(); + }, [ + command.args, + command.commandDefinition.name, + command.input, + isMounted, + store.apiResponse, + ]); + + useEffect(() => { + if (apiResponse && uiResponse) { + setStatus('success'); + } + }, [apiResponse, setStatus, uiResponse]); + + useEffect(() => { + if (apiResponse && store.apiResponse !== apiResponse) { + setStore({ + ...store, + apiResponse, + }); + } + }, [apiResponse, setStore, store]); + + if (store.apiResponse) { + return ( +
+ {uiResponse} + {store.apiResponse as ReactNode} +
+ ); + } + + return null; }, - { - name: 'cmd2', - about: 'runs cmd 2', - args: { - file: { - required: true, - allowMultiples: false, - about: 'Includes file in the run', - validate: () => { - return true; - }, - }, - bad: { - required: false, - allowMultiples: false, - about: 'will fail validation', - validate: () => 'This is a bad value', - }, + args: { + one: { + required: false, + allowMultiples: false, + about: 'just one', }, }, - { - name: 'cmd-long-delay', - about: 'runs cmd 2', - }, - ]; - } - - async executeCommand(command: Command): Promise<{ result: React.ReactNode }> { - await delay(); - - if (command.commandDefinition.name === 'cmd-long-delay') { - await delay(20000); - } - - return { - result: ( -
-
{`${command.commandDefinition.name}`}
-
{`command input: ${command.input}`}
- {JSON.stringify(command.args, null, 2)} -
- ), - }; - } -} + }, + // { + // name: 'get-file', + // about: 'retrieve a file from the endpoint', + // args: { + // file: { + // required: true, + // allowMultiples: false, + // about: 'the file path for the file to be retrieved', + // }, + // }, + // }, + // { + // name: 'cmd2', + // about: 'runs cmd 2', + // args: { + // file: { + // required: true, + // allowMultiples: false, + // about: 'Includes file in the run', + // validate: () => { + // return true; + // }, + // }, + // bad: { + // required: false, + // allowMultiples: false, + // about: 'will fail validation', + // validate: () => 'This is a bad value', + // }, + // }, + // }, + // { + // name: 'cmd-long-delay', + // about: 'runs cmd 2', + // }, + ]; +}; const RunningConsole = memo<{ registeredConsole: RegisteredConsoleClient }>( ({ registeredConsole }) => { @@ -132,8 +208,8 @@ RunningConsole.displayName = 'RunningConsole'; // ------------------------------------------------------------ export const ShowDevConsole = memo(() => { const consoleManager = useConsoleManager(); - const commandService = useMemo(() => { - return new DevCommandService(); + const commands = useMemo(() => { + return getCommandList(); }, []); const handleRegisterOnClick = useCallback(() => { @@ -146,12 +222,12 @@ export const ShowDevConsole = memo(() => { }, consoleProps: { prompt: '>>', - commandService, + commands, 'data-test-subj': 'dev', }, }) .show(); - }, [commandService, consoleManager]); + }, [commands, consoleManager]); return ( @@ -173,8 +249,8 @@ export const ShowDevConsole = memo(() => {

{'Un-managed console'}

- - + + ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 39b1c9aecaf65..c773e0486620a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -133,8 +133,7 @@ const timepickerRanges = [ jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/hooks/use_license'); -// FLAKY: https://github.com/elastic/kibana/issues/115489 -describe.skip('when on the endpoint list page', () => { +describe('when on the endpoint list page', () => { const docGenerator = new EndpointDocGenerator(); const { act, screen, fireEvent, waitFor } = reactTestingLibrary; @@ -296,17 +295,6 @@ describe.skip('when on the endpoint list page', () => { }); describe('when there is no selected host in the url', () => { - it('should not show the flyout', () => { - setEndpointListApiMockImplementation(coreStart.http, { - endpointsResults: [], - }); - - const renderResult = render(); - expect.assertions(1); - return renderResult.findByTestId('endpointDetailsFlyout').catch((e) => { - expect(e).not.toBeNull(); - }); - }); describe('when list data loads', () => { const generatedPolicyStatuses: Array< HostInfo['metadata']['Endpoint']['policy']['applied']['status'] diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/policy_endpoint_count.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/policy_endpoint_count.tsx index 08c2f1909339d..b8b2f98173f21 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/policy_endpoint_count.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/policy_endpoint_count.tsx @@ -25,7 +25,7 @@ export const PolicyEndpointCount = memo< policyId: string; nonLinkCondition: boolean; } ->(({ policyId, nonLinkCondition, children, ...otherProps }) => { +>(({ policyId, nonLinkCondition, 'data-test-subj': dataTestSubj, children, ...otherProps }) => { const filterByPolicyQuery = `(language:kuery,query:'united.endpoint.Endpoint.policy.applied.id : "${policyId}"')`; const { search } = useLocation(); const { getAppUrl } = useAppUrl(); @@ -59,11 +59,15 @@ export const PolicyEndpointCount = memo< const clickHandler = useNavigateByRouterEventHandler(toRoutePathWithBackOptions); if (nonLinkCondition) { - return {children}; + return ( + + {children} + + ); } return ( // eslint-disable-next-line @elastic/eui/href-or-on-click - + {children} ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx index 482fe0ba064bd..3ea50af79a9c3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -99,15 +99,23 @@ describe('When on the policy list page', () => { expect(policyNameCells).toBeTruthy(); expect(policyNameCells.length).toBe(5); }); - it('should show a avatar for the Created by column', () => { + it('should show an avatar and name for the Created by column', () => { + const expectedAvatarName = policies.items[0].created_by; const createdByCells = renderResult.getAllByTestId('created-by-avatar'); + const firstCreatedByName = renderResult.getAllByTestId('created-by-name')[0]; expect(createdByCells).toBeTruthy(); expect(createdByCells.length).toBe(5); + expect(createdByCells[0].textContent).toEqual(expectedAvatarName.charAt(0)); + expect(firstCreatedByName.textContent).toEqual(expectedAvatarName); }); - it('should show a avatar for the Updated by column', () => { + it('should show an avatar and name for the Updated by column', () => { + const expectedAvatarName = policies.items[0].updated_by; const updatedByCells = renderResult.getAllByTestId('updated-by-avatar'); + const firstUpdatedByName = renderResult.getAllByTestId('updated-by-name')[0]; expect(updatedByCells).toBeTruthy(); expect(updatedByCells.length).toBe(5); + expect(updatedByCells[0].textContent).toEqual(expectedAvatarName.charAt(0)); + expect(firstUpdatedByName.textContent).toEqual(expectedAvatarName); }); it('should show the correct endpoint count', () => { const endpointCount = renderResult.getAllByTestId('policyEndpointCountLink'); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 884a8572ecfe2..67398cbe39a44 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -189,7 +189,9 @@ export const PolicyList = memo(() => { - {name} + + {name} + ); @@ -222,7 +224,9 @@ export const PolicyList = memo(() => { - {name} + + {name} + ); diff --git a/x-pack/plugins/security_solution/public/network/pages/details/utils.ts b/x-pack/plugins/security_solution/public/network/pages/details/utils.ts index 98094665cbcd2..044c1d22a6348 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/network/pages/details/utils.ts @@ -42,6 +42,7 @@ export const getBreadcrumbs = ( }), }, ]; + if (params.detailName != null) { breadcrumb = [ ...breadcrumb, diff --git a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx index a52c4e1560893..32ba03abe500b 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx @@ -106,6 +106,7 @@ jest.mock('../../common/lib/kibana', () => { addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + remove: jest.fn(), }), }; }); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx index 2e1b96447f3df..f7f3cc9a81f7f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/alerts_by_status/alerts_by_status.tsx @@ -49,6 +49,11 @@ const StyledLegendFlexItem = styled(EuiFlexItem)` padding-top: 45px; `; +// To Do remove this styled component once togglequery is updated: #131405 +const StyledEuiPanel = styled(EuiPanel)` + height: fit-content; +`; + interface AlertsByStatusProps { signalIndexName: string | null; } @@ -119,7 +124,10 @@ export const AlertsByStatus = ({ signalIndexName }: AlertsByStatusProps) => { return ( <> - + {loading && ( { )} - + ); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/cases_by_status.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/cases_by_status.tsx index 13a2459b44d68..67ed0d43c830c 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/cases_by_status.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/cases_by_status.tsx @@ -6,7 +6,7 @@ */ import React, { useCallback, useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiPanel, EuiText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; import { AxisStyle, Rotation, ScaleType } from '@elastic/charts'; import styled from 'styled-components'; import { FormattedNumber } from '@kbn/i18n-react'; @@ -112,6 +112,10 @@ const Wrapper = styled.div` width: 100%; `; +const StyledEuiPanel = styled(EuiPanel)` + height: 258px; +`; + const CasesByStatusComponent: React.FC = () => { const { toggleStatus, setToggleStatus } = useQueryToggle(CASES_BY_STATUS_ID); const { getAppUrl, navigateTo } = useNavigation(); @@ -151,7 +155,7 @@ const CasesByStatusComponent: React.FC = () => { ); return ( - + { <> - - <> - - {CASES(totalCounts)} - + {' '} + {CASES(totalCounts)} )} @@ -193,7 +194,7 @@ const CasesByStatusComponent: React.FC = () => { )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.test.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.test.tsx index eb5daeeb374c2..37c48487912e0 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.test.tsx @@ -28,16 +28,17 @@ jest.mock('../../../../common/containers/use_global_time', () => { }); jest.mock('../../../../common/lib/kibana'); -const mockGetAllCasesMetrics = jest.fn(); -mockGetAllCasesMetrics.mockResolvedValue({ - count_open_cases: 1, - count_in_progress_cases: 2, - count_closed_cases: 3, +const mockGetCasesStatus = jest.fn(); +mockGetCasesStatus.mockResolvedValue({ + countOpenCases: 1, + countInProgressCases: 2, + countClosedCases: 3, }); -mockGetAllCasesMetrics.mockResolvedValueOnce({ - count_open_cases: 0, - count_in_progress_cases: 0, - count_closed_cases: 0, + +mockGetCasesStatus.mockResolvedValueOnce({ + countOpenCases: 0, + countInProgressCases: 0, + countClosedCases: 0, }); const mockUseKibana = { @@ -46,7 +47,7 @@ const mockUseKibana = { ...mockCasesContract(), api: { cases: { - getAllCasesMetrics: mockGetAllCasesMetrics, + getCasesStatus: mockGetCasesStatus, }, }, }, diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.tsx index 813fcaa848115..580f91dea6d7d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.tsx @@ -5,17 +5,12 @@ * 2.0. */ +import { CasesStatus } from '@kbn/cases-plugin/common/ui'; import { useState, useEffect } from 'react'; import { APP_ID } from '../../../../../common/constants'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { useKibana } from '../../../../common/lib/kibana'; -export interface CasesCounts { - count_open_cases?: number; - count_in_progress_cases?: number; - count_closed_cases?: number; -} - export interface UseCasesByStatusProps { skip?: boolean; } @@ -37,24 +32,28 @@ export const useCasesByStatus = ({ skip = false }) => { const [updatedAt, setUpdatedAt] = useState(Date.now()); const [isLoading, setIsLoading] = useState(true); - const [casesCounts, setCasesCounts] = useState(null); + const [casesCounts, setCasesCounts] = useState(null); useEffect(() => { let isSubscribed = true; const abortCtrl = new AbortController(); const fetchCases = async () => { try { - const casesResponse = await cases.api.cases.getAllCasesMetrics({ - from, - to, - owner: APP_ID, - }); + const casesResponse = await cases.api.cases.getCasesStatus( + { + from, + to, + owner: APP_ID, + }, + abortCtrl.signal + ); + if (isSubscribed) { setCasesCounts(casesResponse); } } catch (error) { if (isSubscribed) { - setCasesCounts({}); + setCasesCounts(null); } } if (isSubscribed) { @@ -80,14 +79,14 @@ export const useCasesByStatus = ({ skip = false }) => { }, [cases.api.cases, from, skip, to]); return { - closed: casesCounts?.count_closed_cases ?? 0, - inProgress: casesCounts?.count_in_progress_cases ?? 0, + closed: casesCounts?.countClosedCases ?? 0, + inProgress: casesCounts?.countInProgressCases ?? 0, isLoading, - open: casesCounts?.count_open_cases ?? 0, + open: casesCounts?.countOpenCases ?? 0, totalCounts: - (casesCounts?.count_closed_cases ?? 0) + - (casesCounts?.count_in_progress_cases ?? 0) + - (casesCounts?.count_open_cases ?? 0), + (casesCounts?.countClosedCases ?? 0) + + (casesCounts?.countInProgressCases ?? 0) + + (casesCounts?.countOpenCases ?? 0), updatedAt, }; }; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/cases_table.test.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/cases_table.test.tsx new file mode 100644 index 0000000000000..4250c20059094 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/cases_table.test.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { render } from '@testing-library/react'; + +import { TestProviders } from '../../../../common/mock'; +import { parsedCasesItems } from './mock_data'; +import { CasesTable } from './cases_table'; +import type { UseCaseItems } from './use_case_items'; + +const mockGetAppUrl = jest.fn(); +jest.mock('../../../../common/lib/kibana/hooks', () => { + const original = jest.requireActual('../../../../common/lib/kibana/hooks'); + return { + ...original, + useNavigation: () => ({ + getAppUrl: mockGetAppUrl, + }), + }; +}); + +type UseCaseItemsReturn = ReturnType; +const defaultCaseItemsReturn: UseCaseItemsReturn = { + items: [], + isLoading: false, + updatedAt: Date.now(), +}; +const mockUseCaseItems = jest.fn(() => defaultCaseItemsReturn); +const mockUseCaseItemsReturn = (overrides: Partial) => { + mockUseCaseItems.mockReturnValueOnce({ + ...defaultCaseItemsReturn, + ...overrides, + }); +}; + +jest.mock('./use_case_items', () => ({ + useCaseItems: () => mockUseCaseItems(), +})); + +const renderComponent = () => + render( + + + + ); + +describe('CasesTable', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render empty table', () => { + const { getByText, getByTestId } = renderComponent(); + + expect(getByTestId('recentlyCreatedCasesPanel')).toBeInTheDocument(); + expect(getByText('No cases to display')).toBeInTheDocument(); + expect(getByTestId('allCasesButton')).toBeInTheDocument(); + }); + + it('should render a loading table', () => { + mockUseCaseItemsReturn({ isLoading: true }); + const { getByText, getByTestId } = renderComponent(); + + expect(getByText('Updating...')).toBeInTheDocument(); + expect(getByTestId('allCasesButton')).toBeInTheDocument(); + expect(getByTestId('recentlyCreatedCasesTable')).toHaveClass('euiBasicTable-loading'); + }); + + it('should render the updated at subtitle', () => { + mockUseCaseItemsReturn({ isLoading: false }); + const { getByText } = renderComponent(); + + expect(getByText('Updated now')).toBeInTheDocument(); + }); + + it('should render the table columns', () => { + mockUseCaseItemsReturn({ items: parsedCasesItems }); + const { getAllByRole } = renderComponent(); + + const columnHeaders = getAllByRole('columnheader'); + expect(columnHeaders.at(0)).toHaveTextContent('Name'); + expect(columnHeaders.at(1)).toHaveTextContent('Note'); + expect(columnHeaders.at(2)).toHaveTextContent('Time'); + expect(columnHeaders.at(3)).toHaveTextContent('Created by'); + expect(columnHeaders.at(4)).toHaveTextContent('Status'); + }); + + it('should render the table items', () => { + mockUseCaseItemsReturn({ items: [parsedCasesItems[0]] }); + const { getByTestId } = renderComponent(); + + expect(getByTestId('recentlyCreatedCaseName')).toHaveTextContent('sdcsd'); + expect(getByTestId('recentlyCreatedCaseNote')).toHaveTextContent('klklk'); + expect(getByTestId('recentlyCreatedCaseTime')).toHaveTextContent('April 25, 2022'); + expect(getByTestId('recentlyCreatedCaseCreatedBy')).toHaveTextContent('elastic'); + expect(getByTestId('recentlyCreatedCaseStatus')).toHaveTextContent('Open'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/cases_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/cases_table.tsx new file mode 100644 index 0000000000000..80d2495f57ab5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/cases_table.tsx @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; + +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiButton, + EuiEmptyPrompt, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { CaseStatuses } from '@kbn/cases-plugin/common'; + +import { SecurityPageName } from '../../../../app/types'; +import { FormattedDate } from '../../../../common/components/formatted_date'; +import { HeaderSection } from '../../../../common/components/header_section'; +import { HoverVisibilityContainer } from '../../../../common/components/hover_visibility_container'; +import { BUTTON_CLASS as INPECT_BUTTON_CLASS } from '../../../../common/components/inspect'; +import { CaseDetailsLink } from '../../../../common/components/links'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { useNavigation, NavigateTo, GetAppUrl } from '../../../../common/lib/kibana'; +import * as i18n from '../translations'; +import { LastUpdatedAt } from '../util'; +import { StatusBadge } from './status_badge'; +import { CaseItem, useCaseItems } from './use_case_items'; + +type GetTableColumns = (params: { + getAppUrl: GetAppUrl; + navigateTo: NavigateTo; +}) => Array>; + +const DETECTION_RESPONSE_RECENT_CASES_QUERY_ID = 'recentlyCreatedCasesQuery'; + +export const CasesTable = React.memo(() => { + const { getAppUrl, navigateTo } = useNavigation(); + const { toggleStatus, setToggleStatus } = useQueryToggle( + DETECTION_RESPONSE_RECENT_CASES_QUERY_ID + ); + const { items, isLoading, updatedAt } = useCaseItems({ + skip: !toggleStatus, + }); + + const navigateToCases = useCallback(() => { + navigateTo({ deepLinkId: SecurityPageName.case }); + }, [navigateTo]); + + const columns = useMemo( + () => getTableColumns({ getAppUrl, navigateTo }), + [getAppUrl, navigateTo] + ); + + return ( + + + } + showInspectButton={false} + /> + + {toggleStatus && ( + <> + {i18n.NO_CASES_FOUND}} titleSize="xs" /> + } + /> + + + {i18n.VIEW_RECENT_CASES} + + + )} + + + ); +}); + +CasesTable.displayName = 'CasesTable'; + +const getTableColumns: GetTableColumns = () => [ + { + field: 'id', + name: i18n.CASES_TABLE_COLUMN_NAME, + truncateText: true, + textOnly: true, + 'data-test-subj': 'recentlyCreatedCaseName', + + render: (id: string, { name }) => {name}, + }, + { + field: 'note', + name: i18n.CASES_TABLE_COLUMN_NOTE, + truncateText: true, + textOnly: true, + render: (note: string) => ( + + {note} + + ), + }, + { + field: 'createdAt', + name: i18n.CASES_TABLE_COLUMN_TIME, + render: (createdAt: string) => ( + + ), + 'data-test-subj': 'recentlyCreatedCaseTime', + }, + { + field: 'createdBy', + name: i18n.CASES_TABLE_COLUMN_CREATED_BY, + render: (createdBy: string) => ( + + {createdBy} + + ), + }, + { + field: 'status', + name: i18n.CASES_TABLE_COLUMN_STATUS, + render: (status: CaseStatuses) => , + 'data-test-subj': 'recentlyCreatedCaseStatus', + }, +]; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_console/index.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/index.ts similarity index 82% rename from x-pack/plugins/security_solution/public/management/components/endpoint_console/index.ts rename to x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/index.ts index 97f7fb61ae607..4151054408345 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_console/index.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { EndpointConsole } from './endpoint_console'; +export { CasesTable } from './cases_table'; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/mock_data.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/mock_data.ts new file mode 100644 index 0000000000000..2547cfa264757 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/mock_data.ts @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseStatuses } from '@kbn/cases-plugin/common'; + +export const mockCasesResult = { + cases: [ + { + id: '0ce2a510-c43a-11ec-98b5-3109bd2a1901', + version: 'Wzg2MDIsMV0=', + comments: [], + totalComment: 0, + totalAlerts: 0, + title: 'sdcsd', + tags: [], + description: 'klklk', + settings: { + syncAlerts: true, + }, + owner: 'securitySolution', + closed_at: '2022-04-25T01:50:40.435Z', + closed_by: { + full_name: null, + email: null, + username: 'elastic', + }, + createdAt: '2022-04-25T01:50:14.499Z', + createdBy: { + username: 'elastic', + email: null, + full_name: null, + }, + status: 'open', + updated_at: '2022-04-25T01:50:40.435Z', + updated_by: { + full_name: null, + email: null, + username: 'elastic', + }, + connector: { + id: 'none', + name: 'none', + type: '.none', + fields: null, + }, + external_service: null, + }, + { + id: 'dc12f930-c178-11ec-98b5-3109bd2a1901', + version: 'Wzg5NjksMV0=', + comments: [], + totalComment: 0, + totalAlerts: 0, + title: 'zzzz', + tags: [], + description: 'sssss', + settings: { + syncAlerts: true, + }, + owner: 'securitySolution', + closed_at: '2022-04-25T13:45:18.317Z', + closed_by: { + full_name: null, + email: null, + username: 'elastic', + }, + createdAt: '2022-04-21T13:42:17.414Z', + createdBy: { + username: 'elastic', + email: null, + full_name: null, + }, + status: 'in-progress', + updated_at: '2022-04-25T13:45:18.317Z', + updated_by: { + full_name: null, + email: null, + username: 'elastic', + }, + connector: { + id: 'none', + name: 'none', + type: '.none', + fields: null, + }, + external_service: null, + }, + { + id: 'd11cbac0-c178-11ec-98b5-3109bd2a1901', + version: 'Wzg5ODQsMV0=', + comments: [], + totalComment: 0, + totalAlerts: 0, + title: 'asxa', + tags: [], + description: 'dsdd', + settings: { + syncAlerts: true, + }, + owner: 'securitySolution', + closed_at: '2022-04-25T13:45:22.539Z', + closed_by: { + full_name: null, + email: null, + username: 'elastic', + }, + createdAt: '2022-04-21T13:41:59.025Z', + createdBy: { + username: 'elastic', + email: null, + full_name: null, + }, + status: 'closed', + updated_at: '2022-04-25T13:45:22.539Z', + updated_by: { + full_name: null, + email: null, + username: 'elastic', + }, + connector: { + id: 'none', + name: 'none', + type: '.none', + fields: null, + }, + external_service: null, + }, + ], +}; + +export const parsedCasesItems = [ + { + name: 'sdcsd', + note: 'klklk', + createdBy: 'elastic', + status: CaseStatuses.open, + createdAt: '2022-04-25T01:50:14.499Z', + id: '0ce2a510-c43a-11ec-98b5-3109bd2a1901', + }, + { + name: 'zzzz', + note: 'sssss', + status: CaseStatuses['in-progress'], + createdAt: '2022-04-21T13:42:17.414Z', + createdBy: 'elastic', + id: 'dc12f930-c178-11ec-98b5-3109bd2a1901', + }, + { + name: 'asxa', + note: 'dsdd', + createdBy: 'elastic', + status: CaseStatuses.closed, + createdAt: '2022-04-21T13:41:59.025Z', + id: 'd11cbac0-c178-11ec-98b5-3109bd2a1901', + }, +]; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/status_badge.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/status_badge.tsx new file mode 100644 index 0000000000000..9ae1281d9e6e7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/status_badge.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 { CaseStatuses } from '@kbn/cases-plugin/common'; +import { EuiBadge } from '@elastic/eui'; + +import * as i18n from '../translations'; + +interface Props { + status: CaseStatuses; +} + +const statuses = { + [CaseStatuses.open]: { + color: 'primary', + label: i18n.STATUS_OPEN, + }, + [CaseStatuses['in-progress']]: { + color: 'warning', + label: i18n.STATUS_IN_PROGRESS, + }, + [CaseStatuses.closed]: { + color: 'default', + label: i18n.STATUS_CLOSED, + }, +} as const; + +export const StatusBadge: React.FC = ({ status }) => { + return ( + + {statuses[status].label} + + ); +}; + +StatusBadge.displayName = 'StatusBadge'; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/use_case_items.test.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/use_case_items.test.ts new file mode 100644 index 0000000000000..d112aaac7d5ad --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/use_case_items.test.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import { mockCasesResult, parsedCasesItems } from './mock_data'; +import { useCaseItems, UseCaseItemsProps } from './use_case_items'; + +import type { UseCaseItems } from './use_case_items'; + +const dateNow = new Date('2022-04-08T12:00:00.000Z').valueOf(); +const mockDateNow = jest.fn().mockReturnValue(dateNow); +Date.now = jest.fn(() => mockDateNow()) as unknown as DateConstructor['now']; + +const defaultCasesReturn = { + cases: [], +}; + +const mockCasesApi = jest.fn().mockResolvedValue(defaultCasesReturn); +const mockKibana = { + services: { + cases: { + api: { + cases: { + find: (...props: unknown[]) => mockCasesApi(...props), + }, + }, + }, + }, +}; +jest.mock('../../../../common/lib/kibana', () => ({ + useKibana: () => mockKibana, +})); + +const from = '2020-07-07T08:20:18.966Z'; +const to = '2020-07-08T08:20:18.966Z'; + +const mockUseGlobalTime = jest.fn().mockReturnValue({ from, to }); +jest.mock('../../../../common/containers/use_global_time', () => { + return { + useGlobalTime: (...props: unknown[]) => mockUseGlobalTime(...props), + }; +}); + +const renderUseCaseItems = (overrides: Partial = {}) => + renderHook>(() => + useCaseItems({ skip: false, ...overrides }) + ); + +describe('useCaseItems', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockDateNow.mockReturnValue(dateNow); + mockCasesApi.mockResolvedValue(defaultCasesReturn); + }); + + it('should return default values', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderUseCaseItems(); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + items: [], + isLoading: false, + updatedAt: dateNow, + }); + }); + + expect(mockCasesApi).toBeCalledWith({ + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', + owner: 'securitySolution', + sortField: 'create_at', + sortOrder: 'desc', + page: 1, + perPage: 4, + }); + }); + + it('should return parsed items', async () => { + mockCasesApi.mockReturnValue(mockCasesResult); + + await act(async () => { + const { result, waitForNextUpdate } = renderUseCaseItems(); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + items: parsedCasesItems, + isLoading: false, + updatedAt: dateNow, + }); + }); + }); + + it('should return new updatedAt', async () => { + const newDateNow = new Date('2022-04-08T14:00:00.000Z').valueOf(); + mockDateNow.mockReturnValue(newDateNow); + mockDateNow.mockReturnValueOnce(dateNow); + mockCasesApi.mockReturnValue(mockCasesResult); + + await act(async () => { + const { result, waitForNextUpdate } = renderUseCaseItems(); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(mockDateNow).toHaveBeenCalled(); + expect(result.current).toEqual({ + items: parsedCasesItems, + isLoading: false, + updatedAt: newDateNow, + }); + }); + }); + + it('should skip the query', () => { + const { result } = renderUseCaseItems({ skip: true }); + + expect(result.current).toEqual({ + items: [], + isLoading: false, + updatedAt: dateNow, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/use_case_items.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/use_case_items.ts new file mode 100644 index 0000000000000..1b19327989729 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/use_case_items.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 { useState, useEffect } from 'react'; + +import { CaseStatuses } from '@kbn/cases-plugin/common'; +import { Cases } from '@kbn/cases-plugin/common/ui'; + +import { APP_ID } from '../../../../../common/constants'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { useKibana } from '../../../../common/lib/kibana'; +import { addError } from '../../../../common/store/app/actions'; + +import * as i18n from '../translations'; + +export interface CaseItem { + name: string; + note: string; + createdAt: string; + createdBy: string; + status: CaseStatuses; + id: string; +} + +export interface UseCaseItemsProps { + skip: boolean; +} + +export type UseCaseItems = (props: UseCaseItemsProps) => { + items: CaseItem[]; + isLoading: boolean; + updatedAt: number; +}; + +export const useCaseItems: UseCaseItems = ({ skip }) => { + const { + services: { cases }, + } = useKibana(); + const { to, from } = useGlobalTime(); + const [isLoading, setIsLoading] = useState(true); + const [updatedAt, setUpdatedAt] = useState(Date.now()); + const [items, setItems] = useState([]); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + const fetchCases = async () => { + try { + const casesResponse = await cases.api.cases.find({ + from, + to, + owner: APP_ID, + sortField: 'create_at', + sortOrder: 'desc', + page: 1, + perPage: 4, + }); + + if (isSubscribed) { + setItems(parseCases(casesResponse)); + setUpdatedAt(Date.now()); + } + } catch (error) { + if (isSubscribed) { + addError(error, { title: i18n.ERROR_MESSAGE_CASES }); + } + } + setIsLoading(false); + }; + + if (!skip) { + fetchCases(); + } + + if (skip) { + setIsLoading(false); + isSubscribed = false; + abortCtrl.abort(); + } + + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [cases.api.cases, from, skip, to]); + + return { items, isLoading, updatedAt }; +}; + +function parseCases(casesResponse: Cases): CaseItem[] { + const allCases = casesResponse.cases || []; + + return allCases.reduce((accumulated, currentCase) => { + accumulated.push({ + id: currentCase.id, + name: currentCase.title, + note: currentCase.description, + createdAt: currentCase.createdAt, + createdBy: currentCase.createdBy.username || '—', + status: currentCase.status, + }); + + return accumulated; + }, []); +} diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx index 4294f23f796bd..a89055a72df6f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.test.tsx @@ -47,7 +47,8 @@ const renderComponent = () => ); -describe('HostAlertsTable', () => { +// FLAKY: https://github.com/elastic/kibana/issues/131611 +describe.skip('HostAlertsTable', () => { beforeEach(() => { jest.clearAllMocks(); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx index 4fe5bf7785bc7..49ad4352cb586 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx @@ -6,6 +6,7 @@ */ import React, { useCallback, useMemo } from 'react'; +import styled from 'styled-components'; import { EuiBasicTable, @@ -40,6 +41,11 @@ interface HostAlertsTableProps { const DETECTION_RESPONSE_HOST_SEVERITY_QUERY_ID = 'vulnerableHostsBySeverityQuery'; +// To Do remove this styled component once togglequery is updated: #131405 +const StyledEuiPanel = styled(EuiPanel)` + height: fit-content; +`; + export const HostAlertsTable = React.memo(({ signalIndexName }: HostAlertsTableProps) => { const { getAppUrl, navigateTo } = useNavigation(); const { toggleStatus, setToggleStatus } = useQueryToggle( @@ -62,8 +68,9 @@ export const HostAlertsTable = React.memo(({ signalIndexName }: HostAlertsTableP ); return ( + //
- + )} - + + //
); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/use_host_alerts_items.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/use_host_alerts_items.ts index ebd7af1b49cc2..a01cd709b44f6 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/use_host_alerts_items.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/use_host_alerts_items.ts @@ -191,7 +191,7 @@ function parseHostsData( return [ ...accumalatedAlertsByHost, { - hostName: currentHost.key, + hostName: currentHost.key || '—', totalAlerts: currentHost.doc_count, low: currentHost.low.doc_count, medium: currentHost.medium.doc_count, diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/translations.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/translations.ts index 8f37c2e641c7a..d013ab258631a 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/translations.ts @@ -103,12 +103,26 @@ export const USER_ALERTS_SECTION_TITLE = i18n.translate( } ); +export const CASES_TABLE_SECTION_TITLE = i18n.translate( + 'xpack.securitySolution.detectionResponse.caseSectionTitle', + { + defaultMessage: 'Recently created cases', + } +); + export const NO_ALERTS_FOUND = i18n.translate( 'xpack.securitySolution.detectionResponse.noRuleAlerts', { defaultMessage: 'No alerts to display', } ); + +export const NO_CASES_FOUND = i18n.translate( + 'xpack.securitySolution.detectionResponse.noRecentCases', + { + defaultMessage: 'No cases to display', + } +); export const RULE_ALERTS_COLUMN_RULE_NAME = i18n.translate( 'xpack.securitySolution.detectionResponse.ruleAlertsColumnRuleName', { @@ -156,14 +170,21 @@ export const OPEN_ALL_ALERTS_BUTTON = i18n.translate( export const VIEW_ALL_USER_ALERTS = i18n.translate( 'xpack.securitySolution.detectionResponse.viewAllUserAlerts', { - defaultMessage: 'View all other user alerts', + defaultMessage: 'View all users', + } +); + +export const VIEW_RECENT_CASES = i18n.translate( + 'xpack.securitySolution.detectionResponse.viewRecentCases', + { + defaultMessage: 'View recent cases', } ); export const VIEW_ALL_HOST_ALERTS = i18n.translate( 'xpack.securitySolution.detectionResponse.viewAllHostAlerts', { - defaultMessage: 'View all other host alerts', + defaultMessage: 'View all hosts', } ); @@ -180,3 +201,45 @@ export const USER_ALERTS_USERNAME_COLUMN = i18n.translate( defaultMessage: 'User name', } ); + +export const CASES_TABLE_COLUMN_NAME = i18n.translate( + 'xpack.securitySolution.detectionResponse.caseColumnName', + { + defaultMessage: 'Name', + } +); + +export const CASES_TABLE_COLUMN_NOTE = i18n.translate( + 'xpack.securitySolution.detectionResponse.caseColumnNote', + { + defaultMessage: 'Note', + } +); + +export const CASES_TABLE_COLUMN_TIME = i18n.translate( + 'xpack.securitySolution.detectionResponse.caseColumnTime', + { + defaultMessage: 'Time', + } +); + +export const CASES_TABLE_COLUMN_CREATED_BY = i18n.translate( + 'xpack.securitySolution.detectionResponse.caseColumnCreatedBy', + { + defaultMessage: 'Created by', + } +); + +export const CASES_TABLE_COLUMN_STATUS = i18n.translate( + 'xpack.securitySolution.detectionResponse.caseColumnStatus', + { + defaultMessage: 'Status', + } +); + +export const ERROR_MESSAGE_CASES = i18n.translate( + 'xpack.securitySolution.detectionResponse.errorMessage', + { + defaultMessage: 'Error fetching case data', + } +); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/use_user_alerts_items.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/use_user_alerts_items.ts index e86ee2f2aef95..3e2778f82cde4 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/use_user_alerts_items.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/use_user_alerts_items.ts @@ -190,7 +190,7 @@ function parseUsersData(rawAggregation: AlertCountersBySeverityAggregation): Use return [ ...accumalatedAlertsByUser, { - userName: currentUser.key, + userName: currentUser.key || '—', totalAlerts: currentUser.doc_count, low: currentUser.low.doc_count, medium: currentUser.medium.doc_count, diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx index e54ef27c0db34..0416bb48e13e1 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx @@ -6,6 +6,7 @@ */ import React, { useCallback, useMemo } from 'react'; +import styled from 'styled-components'; import { EuiBasicTable, @@ -40,6 +41,11 @@ type GetTableColumns = (params: { const DETECTION_RESPONSE_USER_SEVERITY_QUERY_ID = 'vulnerableUsersBySeverityQuery'; +// To Do remove this styled component once togglequery is updated: #131405 +const StyledEuiPanel = styled(EuiPanel)` + height: fit-content; +`; + export const UserAlertsTable = React.memo(({ signalIndexName }: UserAlertsTableProps) => { const { getAppUrl, navigateTo } = useNavigation(); const { toggleStatus, setToggleStatus } = useQueryToggle( @@ -62,7 +68,7 @@ export const UserAlertsTable = React.memo(({ signalIndexName }: UserAlertsTableP return ( - + )} - + ); }); diff --git a/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx index 4ad2eeac35240..3b88d22eb4da9 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx @@ -11,12 +11,24 @@ import { render } from '@testing-library/react'; import { DetectionResponse } from './detection_response'; import { TestProviders } from '../../common/mock'; +jest.mock('../components/detection_response/alerts_by_status', () => ({ + AlertsByStatus: () =>
, +})); + +jest.mock('../components/detection_response/cases_table', () => ({ + CasesTable: () =>
, +})); + +jest.mock('../components/detection_response/host_alerts_table', () => ({ + HostAlertsTable: () =>
, +})); + jest.mock('../components/detection_response/rule_alerts_table', () => ({ RuleAlertsTable: () =>
, })); -jest.mock('../components/detection_response/alerts_by_status', () => ({ - AlertsByStatus: () =>
, +jest.mock('../components/detection_response/user_alerts_table', () => ({ + UserAlertsTable: () =>
, })); jest.mock('../components/detection_response/cases_by_status', () => ({ @@ -37,14 +49,22 @@ jest.mock('../../common/containers/sourcerer', () => ({ useSourcererDataView: () => mockUseSourcererDataView(), })); -const defaultUseUserInfoReturn = { - signalIndexName: '', - canUserREAD: true, +const defaultUseAlertsPrivilegesReturn = { + hasKibanaREAD: true, hasIndexRead: true, }; -const mockUseUserInfo = jest.fn(() => defaultUseUserInfoReturn); -jest.mock('../../detections/components/user_info', () => ({ - useUserInfo: () => mockUseUserInfo(), + +const defaultUseSignalIndexReturn = { + signalIndexName: '', +}; + +const mockUseSignalIndex = jest.fn(() => defaultUseSignalIndexReturn); +jest.mock('../../detections/containers/detection_engine/alerts/use_signal_index', () => ({ + useSignalIndex: () => mockUseSignalIndex(), +})); +const mockUseAlertsPrivileges = jest.fn(() => defaultUseAlertsPrivilegesReturn); +jest.mock('../../detections/containers/detection_engine/alerts/use_alerts_privileges', () => ({ + useAlertsPrivileges: () => mockUseAlertsPrivileges(), })); const defaultUseCasesPermissionsReturn = { read: true }; @@ -61,7 +81,8 @@ describe('DetectionResponse', () => { beforeEach(() => { jest.clearAllMocks(); mockUseSourcererDataView.mockReturnValue(defaultUseSourcererReturn); - mockUseUserInfo.mockReturnValue(defaultUseUserInfoReturn); + mockUseAlertsPrivileges.mockReturnValue(defaultUseAlertsPrivilegesReturn); + mockUseSignalIndex.mockReturnValue(defaultUseSignalIndexReturn); mockUseCasesPermissions.mockReturnValue(defaultUseCasesPermissionsReturn); }); @@ -121,9 +142,9 @@ describe('DetectionResponse', () => { }); it('should not render alerts data sections if user has not index read permission', () => { - mockUseUserInfo.mockReturnValue({ - ...defaultUseUserInfoReturn, + mockUseAlertsPrivileges.mockReturnValue({ hasIndexRead: false, + hasKibanaREAD: true, }); const result = render( @@ -135,18 +156,19 @@ describe('DetectionResponse', () => { ); expect(result.queryByTestId('detectionResponsePage')).toBeInTheDocument(); - // TODO: assert other alert sections are not in the document + expect(result.queryByTestId('mock_CasesTable')).toBeInTheDocument(); + expect(result.queryByTestId('mock_CasesByStatus')).toBeInTheDocument(); + expect(result.queryByTestId('mock_RuleAlertsTable')).not.toBeInTheDocument(); + expect(result.queryByTestId('mock_HostAlertsTable')).not.toBeInTheDocument(); + expect(result.queryByTestId('mock_UserAlertsTable')).not.toBeInTheDocument(); expect(result.queryByTestId('mock_AlertsByStatus')).not.toBeInTheDocument(); - - // TODO: assert cases sections are in the document - expect(result.queryByTestId('mock_CasesByStatus')).toBeInTheDocument(); }); it('should not render alerts data sections if user has not kibana read permission', () => { - mockUseUserInfo.mockReturnValue({ - ...defaultUseUserInfoReturn, - canUserREAD: false, + mockUseAlertsPrivileges.mockReturnValue({ + hasIndexRead: true, + hasKibanaREAD: false, }); const result = render( @@ -158,13 +180,13 @@ describe('DetectionResponse', () => { ); expect(result.queryByTestId('detectionResponsePage')).toBeInTheDocument(); + expect(result.queryByTestId('mock_CasesTable')).toBeInTheDocument(); + expect(result.queryByTestId('mock_CasesByStatus')).toBeInTheDocument(); - // TODO: assert all alert sections are not in the document expect(result.queryByTestId('mock_RuleAlertsTable')).not.toBeInTheDocument(); + expect(result.queryByTestId('mock_HostAlertsTable')).not.toBeInTheDocument(); + expect(result.queryByTestId('mock_UserAlertsTable')).not.toBeInTheDocument(); expect(result.queryByTestId('mock_AlertsByStatus')).not.toBeInTheDocument(); - - // TODO: assert all cases sections are in the document - expect(result.queryByTestId('mock_CasesByStatus')).toBeInTheDocument(); }); it('should not render cases data sections if user has not cases read permission', () => { @@ -178,18 +200,20 @@ describe('DetectionResponse', () => { ); + expect(result.queryByTestId('mock_CasesTable')).not.toBeInTheDocument(); + expect(result.queryByTestId('mock_CasesByStatus')).not.toBeInTheDocument(); + expect(result.queryByTestId('detectionResponsePage')).toBeInTheDocument(); - // TODO: assert all alert sections are in the document expect(result.queryByTestId('mock_RuleAlertsTable')).toBeInTheDocument(); + expect(result.queryByTestId('mock_HostAlertsTable')).toBeInTheDocument(); + expect(result.queryByTestId('mock_UserAlertsTable')).toBeInTheDocument(); expect(result.queryByTestId('mock_AlertsByStatus')).toBeInTheDocument(); - // TODO: assert all cases sections are not in the document - expect(result.queryByTestId('mock_CasesByStatus')).not.toBeInTheDocument(); }); it('should render page permissions message if user has any read permission', () => { mockUseCasesPermissions.mockReturnValue({ read: false }); - mockUseUserInfo.mockReturnValue({ - ...defaultUseUserInfoReturn, + mockUseAlertsPrivileges.mockReturnValue({ + hasKibanaREAD: true, hasIndexRead: false, }); diff --git a/x-pack/plugins/security_solution/public/overview/pages/detection_response.tsx b/x-pack/plugins/security_solution/public/overview/pages/detection_response.tsx index 5b9d298bf915c..fedcca0aac7b4 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/detection_response.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/detection_response.tsx @@ -11,17 +11,20 @@ import { SecuritySolutionPageWrapper } from '../../common/components/page_wrappe import { SpyRoute } from '../../common/utils/route/spy_routes'; import { SecurityPageName } from '../../app/types'; import { useSourcererDataView } from '../../common/containers/sourcerer'; -import { useUserInfo } from '../../detections/components/user_info'; import { HeaderPage } from '../../common/components/header_page'; import { useKibana, useGetUserCasesPermissions } from '../../common/lib/kibana'; -import { HostAlertsTable, UserAlertsTable } from '../components/detection_response'; +import { EmptyPage } from '../../common/components/empty_page'; import { LandingPageComponent } from '../../common/components/landing_page'; +import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; +import { useAlertsPrivileges } from '../../detections/containers/detection_engine/alerts/use_alerts_privileges'; +import { AlertsByStatus } from '../components/detection_response/alerts_by_status'; +import { HostAlertsTable } from '../components/detection_response/host_alerts_table'; import { RuleAlertsTable } from '../components/detection_response/rule_alerts_table'; +import { UserAlertsTable } from '../components/detection_response/user_alerts_table'; import * as i18n from './translations'; -import { EmptyPage } from '../../common/components/empty_page'; +import { CasesTable } from '../components/detection_response/cases_table'; import { CasesByStatus } from '../components/detection_response/cases_by_status'; -import { AlertsByStatus } from '../components/detection_response/alerts_by_status'; const NoPrivilegePage: React.FC = () => { const { docLinks } = useKibana().services; @@ -48,9 +51,10 @@ const NoPrivilegePage: React.FC = () => { const DetectionResponseComponent = () => { const { indicesExist, indexPattern, loading: isSourcererLoading } = useSourcererDataView(); - const { signalIndexName, canUserREAD, hasIndexRead } = useUserInfo(); + const { signalIndexName } = useSignalIndex(); + const { hasKibanaREAD, hasIndexRead } = useAlertsPrivileges(); const canReadCases = useGetUserCasesPermissions()?.read; - const canReadAlerts = canUserREAD && hasIndexRead; + const canReadAlerts = hasKibanaREAD && hasIndexRead; if (!canReadAlerts && !canReadCases) { return ; @@ -90,7 +94,11 @@ const DetectionResponseComponent = () => { )} - {canReadCases && {'[cases table]'}} + {canReadCases && ( + + + + )} {canReadAlerts && ( diff --git a/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx b/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx index 8ddd081088686..368bf2589d5c7 100644 --- a/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx +++ b/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx @@ -103,7 +103,7 @@ export const useUserRiskScore = ({ const spaceId = useSpaceId(); const defaultIndex = spaceId ? getUserRiskIndex(spaceId, onlyLatest) : undefined; - const usersFeatureEnabled = useIsExperimentalFeatureEnabled('usersEnabled'); + const riskyUsersFeatureEnabled = useIsExperimentalFeatureEnabled('riskyUsersEnabled'); return useRiskScore({ timerange, onlyLatest, @@ -111,7 +111,7 @@ export const useUserRiskScore = ({ sort, skip, pagination, - featureEnabled: usersFeatureEnabled, + featureEnabled: riskyUsersFeatureEnabled, defaultIndex, }); }; diff --git a/x-pack/plugins/security_solution/public/risk_score/containers/index.ts b/x-pack/plugins/security_solution/public/risk_score/containers/index.ts index 089c88aa9be37..ffe964b974776 100644 --- a/x-pack/plugins/security_solution/public/risk_score/containers/index.ts +++ b/x-pack/plugins/security_solution/public/risk_score/containers/index.ts @@ -12,12 +12,12 @@ export * from './kpi'; export const enum UserRiskScoreQueryId { USERS_BY_RISK = 'UsersByRisk', + USER_DETAILS_RISK_SCORE = 'UserDetailsRiskScore', } export const enum HostRiskScoreQueryId { DEFAULT = 'HostRiskScore', - HOST_RISK_SCORE_OVER_TIME = 'HostRiskScoreOverTimeQuery', - TOP_HOST_SCORE_CONTRIBUTORS = 'TopHostScoreContributorsQuery', + HOST_DETAILS_RISK_SCORE = 'HostDetailsRiskScore', OVERVIEW_RISKY_HOSTS = 'OverviewRiskyHosts', HOSTS_BY_RISK = 'HostsByRisk', } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index 6412567174c73..c62869c0f0746 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -275,6 +275,7 @@ export const QueryBarTimeline = memo( savedQuery={savedQuery} onSavedQuery={onSavedQuery} dataTestSubj={'timelineQueryInput'} + displayStyle="inPage" /> ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index 17dd6491f2326..c5cc33c18c1c4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -62,24 +62,13 @@ interface Props { const SearchOrFilterContainer = styled.div` ${({ theme }) => `margin-top: ${theme.eui.euiSizeXS};`} - user-select: none; - .globalQueryBar { - padding: 0px; - .kbnQueryBar { - div:first-child { - margin-right: 0px; - } - } - .globalFilterGroup__wrapper.globalFilterGroup__wrapper-isVisible { - height: auto !important; - } - } + user-select: none; // This should not be here, it makes the entire page inaccessible `; SearchOrFilterContainer.displayName = 'SearchOrFilterContainer'; const ModeFlexItem = styled(EuiFlexItem)` - user-select: none; + user-select: none; // Again, why? `; ModeFlexItem.displayName = 'ModeFlexItem'; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx index dd032016088b6..ee4af121dd054 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.test.tsx @@ -35,6 +35,7 @@ jest.mock('../../common/lib/kibana', () => ({ addError: jest.fn(), addSuccess: jest.fn(), addWarning: jest.fn(), + remove: jest.fn(), }), useKibana: jest.fn().mockReturnValue({ services: { diff --git a/x-pack/plugins/security_solution/public/users/components/kpi_users/index.tsx b/x-pack/plugins/security_solution/public/users/components/kpi_users/index.tsx index 11d577a037ddb..768248cf9b6ac 100644 --- a/x-pack/plugins/security_solution/public/users/components/kpi_users/index.tsx +++ b/x-pack/plugins/security_solution/public/users/components/kpi_users/index.tsx @@ -6,17 +6,47 @@ */ import React from 'react'; -import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import { UsersKpiProps } from './types'; import { HostsKpiAuthentications } from '../../../hosts/components/kpi_hosts/authentications'; import { TotalUsersKpi } from './total_users'; +import { useUserRiskScore } from '../../../risk_score/containers'; +import { CallOutSwitcher } from '../../../common/components/callouts'; +import * as i18n from './translations'; export const UsersKpiComponent = React.memo( ({ filterQuery, from, indexNames, to, setQuery, skip, narrowDateRange }) => { + const [_, { isModuleEnabled }] = useUserRiskScore({}); + return ( <> + {isModuleEnabled === false && ( + <> + + {/* + TODO PENDING ON USER RISK DOCUMENTATION} + */} + {i18n.LEARN_MORE} {i18n.USER_RISK_DATA} + {/* */} + + + ), + }} + /> + + + )} ( } ); -UsersKpiComponent.displayName = 'HostsKpiComponent'; +UsersKpiComponent.displayName = 'UsersKpiComponent'; diff --git a/x-pack/plugins/security_solution/public/users/components/kpi_users/translations.ts b/x-pack/plugins/security_solution/public/users/components/kpi_users/translations.ts new file mode 100644 index 0000000000000..8315b6dc21c19 --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/components/kpi_users/translations.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 { i18n } from '@kbn/i18n'; + +export const ENABLE_USER_RISK_TEXT = i18n.translate( + 'xpack.securitySolution.kpiUser.enableUserRiskText', + { + defaultMessage: 'Enable user risk module to see more data', + } +); + +export const LEARN_MORE = i18n.translate('xpack.securitySolution.kpiUser.learnMore', { + defaultMessage: 'Learn more about', +}); + +export const USER_RISK_DATA = i18n.translate('xpack.securitySolution.kpiUser.userRiskData', { + defaultMessage: 'user risk data', +}); diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.test.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.test.tsx new file mode 100644 index 0000000000000..764d732fa2898 --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.test.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 { render, fireEvent } from '@testing-library/react'; +import React from 'react'; +import { UserRiskInformationButtonEmpty } from '.'; +import { TestProviders } from '../../../common/mock'; + +describe('User Risk Flyout', () => { + describe('UserRiskInformationButtonEmpty', () => { + it('renders', () => { + const { queryByTestId } = render(); + + expect(queryByTestId('open-risk-information-flyout-trigger')).toBeInTheDocument(); + }); + }); + + it('opens and displays table with 5 rows', () => { + const NUMBER_OF_ROWS = 1 + 5; // 1 header row + 5 severity rows + const { getByTestId, queryByTestId, queryAllByRole } = render( + + + + ); + + fireEvent.click(getByTestId('open-risk-information-flyout-trigger')); + + expect(queryByTestId('risk-information-table')).toBeInTheDocument(); + expect(queryAllByRole('row')).toHaveLength(NUMBER_OF_ROWS); + }); +}); diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.tsx new file mode 100644 index 0000000000000..6ae647544d965 --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_information/index.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + useGeneratedHtmlId, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiText, + EuiTitle, + EuiBasicTable, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutFooter, + EuiButton, + EuiSpacer, + EuiBasicTableColumn, + EuiButtonEmpty, +} from '@elastic/eui'; + +import React from 'react'; + +import * as i18n from './translations'; +import { useOnOpenCloseHandler } from '../../../helper_hooks'; +import { RiskScore } from '../../../common/components/severity/common'; +import { RiskSeverity } from '../../../../common/search_strategy'; + +const tableColumns: Array> = [ + { + field: 'classification', + name: i18n.INFORMATION_CLASSIFICATION_HEADER, + render: (riskScore?: RiskSeverity) => { + if (riskScore != null) { + return ; + } + }, + }, + { + field: 'range', + name: i18n.INFORMATION_RISK_HEADER, + }, +]; + +interface TableItem { + range?: string; + classification: RiskSeverity; +} + +const tableItems: TableItem[] = [ + { classification: RiskSeverity.critical, range: i18n.CRITICAL_RISK_DESCRIPTION }, + { classification: RiskSeverity.high, range: '70 - 90 ' }, + { classification: RiskSeverity.moderate, range: '40 - 70' }, + { classification: RiskSeverity.low, range: '20 - 40' }, + { classification: RiskSeverity.unknown, range: i18n.UNKNOWN_RISK_DESCRIPTION }, +]; + +export const USER_RISK_INFO_BUTTON_CLASS = 'UserRiskInformation__button'; + +export const UserRiskInformationButtonEmpty = () => { + const [isFlyoutVisible, handleOnOpen, handleOnClose] = useOnOpenCloseHandler(); + + return ( + <> + + {i18n.INFO_BUTTON_TEXT} + + {isFlyoutVisible && } + + ); +}; + +const UserRiskInformationFlyout = ({ handleOnClose }: { handleOnClose: () => void }) => { + const simpleFlyoutTitleId = useGeneratedHtmlId({ + prefix: 'UserRiskInformation', + }); + + return ( + + + +

{i18n.TITLE}

+
+
+ + +

{i18n.INTRODUCTION}

+

{i18n.EXPLANATION_MESSAGE}

+
+ + + {/* TODO PENDING ON USER RISK DOCUMENTATION + + + + + ), + }} + /> */} +
+ + + + + {i18n.CLOSE_BUTTON_LTEXT} + + + +
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_information/translations.ts b/x-pack/plugins/security_solution/public/users/components/user_risk_information/translations.ts new file mode 100644 index 0000000000000..dbf4ad96e486c --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_information/translations.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 { i18n } from '@kbn/i18n'; + +export const INFORMATION_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.informationAriaLabel', + { + defaultMessage: 'Information', + } +); + +export const INFORMATION_CLASSIFICATION_HEADER = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.classificationHeader', + { + defaultMessage: 'Classification', + } +); + +export const INFORMATION_RISK_HEADER = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.riskHeader', + { + defaultMessage: 'User risk score range', + } +); + +export const UNKNOWN_RISK_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.unknownRiskDescription', + { + defaultMessage: 'Less than 20', + } +); + +export const CRITICAL_RISK_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.criticalRiskDescription', + { + defaultMessage: '90 and above', + } +); + +export const TITLE = i18n.translate('xpack.securitySolution.users.userRiskInformation.title', { + defaultMessage: 'How is user risk calculated?', +}); + +export const INTRODUCTION = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.introduction', + { + defaultMessage: + 'The User Risk Score capability surfaces risky users from within your environment.', + } +); + +export const EXPLANATION_MESSAGE = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.explanation', + { + defaultMessage: + 'This feature utilizes a transform, with a scripted metric aggregation to calculate user risk scores based on detection rule alerts with an "open" status, within a 5 day time window. The transform runs hourly to keep the score updated as new detection rule alerts stream in.', + } +); + +export const CLOSE_BUTTON_LTEXT = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.closeBtn', + { + defaultMessage: 'Close', + } +); + +export const INFO_BUTTON_TEXT = i18n.translate( + 'xpack.securitySolution.users.userRiskInformation.buttonLabel', + { + defaultMessage: 'How is risk score calculated?', + } +); diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.tsx index c3b26aa1e44d3..3ea4d6a14c247 100644 --- a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/columns.tsx @@ -22,6 +22,7 @@ import * as i18n from './translations'; import { RiskScore } from '../../../common/components/severity/common'; import { RiskSeverity } from '../../../../common/search_strategy'; import { UserDetailsLink } from '../../../common/components/links'; +import { UsersTableType } from '../../store/model'; export const getUserRiskScoreColumns = ({ dispatchSeverityUpdate, @@ -55,7 +56,7 @@ export const getUserRiskScoreColumns = ({ ) : ( - + ) } /> diff --git a/x-pack/plugins/security_solution/public/users/pages/constants.ts b/x-pack/plugins/security_solution/public/users/pages/constants.ts index 44d3b0ba83e1f..e8b9e4a4118a1 100644 --- a/x-pack/plugins/security_solution/public/users/pages/constants.ts +++ b/x-pack/plugins/security_solution/public/users/pages/constants.ts @@ -12,4 +12,4 @@ export const usersDetailsPagePath = `${USERS_PATH}/:detailName`; export const usersTabPath = `${USERS_PATH}/:tabName(${UsersTableType.allUsers}|${UsersTableType.authentications}|${UsersTableType.anomalies}|${UsersTableType.risk}|${UsersTableType.events}|${UsersTableType.alerts})`; -export const usersDetailsTabPath = `${usersDetailsPagePath}/:tabName(${UsersTableType.authentications}|${UsersTableType.anomalies}|${UsersTableType.events}|${UsersTableType.alerts})`; +export const usersDetailsTabPath = `${usersDetailsPagePath}/:tabName(${UsersTableType.authentications}|${UsersTableType.anomalies}|${UsersTableType.events}|${UsersTableType.alerts}|${UsersTableType.risk})`; diff --git a/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx index d3c3a4607b39c..22b394f41bfaf 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx @@ -21,6 +21,7 @@ import { EventsQueryTabBody } from '../../../common/components/events_tab/events import { AlertsView } from '../../../common/components/alerts_viewer'; import { userNameExistsFilter } from './helpers'; import { AuthenticationsQueryTabBody } from '../navigation'; +import { UserRiskTabBody } from '../navigation/user_risk_tab_body'; export const UsersDetailsTabs = React.memo( ({ @@ -107,6 +108,9 @@ export const UsersDetailsTabs = React.memo( {...tabProps} /> + + + ); } diff --git a/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx index ee070f749925e..9f12d8824f817 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx @@ -43,6 +43,12 @@ export const navTabsUsersDetails = ( href: getTabsOnUsersDetailsUrl(userName, UsersTableType.alerts), disabled: false, }, + [UsersTableType.risk]: { + id: UsersTableType.risk, + name: i18n.NAVIGATION_RISK_TITLE, + href: getTabsOnUsersDetailsUrl(userName, UsersTableType.risk), + disabled: false, + }, }; return hasMlUserPermissions diff --git a/x-pack/plugins/security_solution/public/users/pages/details/utils.ts b/x-pack/plugins/security_solution/public/users/pages/details/utils.ts index 4b85d0f59314f..26ed75997a85d 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/users/pages/details/utils.ts @@ -27,6 +27,7 @@ const TabNameMappedToI18nKey: Record = { [UsersTableType.risk]: i18n.NAVIGATION_RISK_TITLE, [UsersTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, [UsersTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, + [UsersTableType.risk]: i18n.NAVIGATION_RISK_TITLE, }; export const getBreadcrumbs = ( diff --git a/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx index 3097fdeb604f3..046b8b7088125 100644 --- a/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx @@ -38,12 +38,6 @@ export const navTabsUsers = ( href: getTabsOnUsersUrl(UsersTableType.anomalies), disabled: false, }, - [UsersTableType.risk]: { - id: UsersTableType.risk, - name: i18n.NAVIGATION_RISK_TITLE, - href: getTabsOnUsersUrl(UsersTableType.risk), - disabled: false, - }, [UsersTableType.events]: { id: UsersTableType.events, name: i18n.NAVIGATION_EVENTS_TITLE, @@ -56,6 +50,12 @@ export const navTabsUsers = ( href: getTabsOnUsersUrl(UsersTableType.alerts), disabled: false, }, + [UsersTableType.risk]: { + id: UsersTableType.risk, + name: i18n.NAVIGATION_RISK_TITLE, + href: getTabsOnUsersUrl(UsersTableType.risk), + disabled: false, + }, }; if (!hasMlUserPermissions) { diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx index 6b5ec66f864bb..10c85be1b72f7 100644 --- a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx @@ -29,23 +29,22 @@ describe('All users query tab body', () => { endDate: '2019-06-25T06:31:59.345Z', type: UsersType.page, }; + beforeEach(() => { jest.clearAllMocks(); mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseUserRiskScore.mockReturnValue([ false, { - authentications: [], - id: '123', inspect: { dsl: [], response: [], }, isInspected: false, totalCount: 0, - pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, - loadPage: jest.fn(), refetch: jest.fn(), + isModuleEnabled: true, }, ]); mockUseUserRiskScoreKpi.mockReturnValue({ @@ -59,6 +58,7 @@ describe('All users query tab body', () => { }, }); }); + it('toggleStatus=true, do not skip', () => { render( @@ -68,6 +68,7 @@ describe('All users query tab body', () => { expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(false); expect(mockUseUserRiskScoreKpi.mock.calls[0][0].skip).toEqual(false); }); + it('toggleStatus=false, skip', () => { mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); render( diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.test.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.test.tsx new file mode 100644 index 0000000000000..539b6df2d8f0a --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../common/mock'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { UsersType } from '../../store/model'; +import { useUserRiskScore } from '../../../risk_score/containers'; +import { UserRiskTabBody } from './user_risk_tab_body'; + +jest.mock('../../../risk_score/containers'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('../../../common/lib/kibana'); + +describe('User query tab body', () => { + const mockUseUserRiskScore = useUserRiskScore as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + userName: 'testUser', + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: UsersType.page, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + mockUseUserRiskScore.mockReturnValue([ + false, + { + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + refetch: jest.fn(), + isModuleEnabled: true, + }, + ]); + }); + + it("doesn't skip when both toggleStatus are true", () => { + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: true, setToggleStatus: jest.fn() }); + + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(false); + }); + + it("doesn't skip when at least one toggleStatus is true", () => { + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: false, setToggleStatus: jest.fn() }); + + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(false); + }); + + it('does skip when at both toggleStatus are false', () => { + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: false, setToggleStatus: jest.fn() }); + mockUseQueryToggle.mockReturnValueOnce({ toggleStatus: false, setToggleStatus: jest.fn() }); + + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx new file mode 100644 index 0000000000000..ee37df16fd19c --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_tab_body.tsx @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import styled from 'styled-components'; + +import * as i18n from '../translations'; + +import { useQueryInspector } from '../../../common/components/page/manage_query'; +import { RiskScoreOverTime } from '../../../common/components/risk_score_over_time'; +import { TopRiskScoreContributors } from '../../../common/components/top_risk_score_contributors'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { UserRiskScoreQueryId, useUserRiskScore } from '../../../risk_score/containers'; +import { buildUserNamesFilter } from '../../../../common/search_strategy'; +import { UsersComponentsQueryProps } from './types'; +import { UserRiskInformationButtonEmpty } from '../../components/user_risk_information'; + +const QUERY_ID = UserRiskScoreQueryId.USER_DETAILS_RISK_SCORE; + +const StyledEuiFlexGroup = styled(EuiFlexGroup)` + margin-top: ${({ theme }) => theme.eui.paddingSizes.l}; +`; + +const UserRiskTabBodyComponent: React.FC< + Pick & { + userName: string; + } +> = ({ userName, startDate, endDate, setQuery, deleteQuery }) => { + const timerange = useMemo( + () => ({ + from: startDate, + to: endDate, + }), + [startDate, endDate] + ); + + const { toggleStatus: overTimeToggleStatus, setToggleStatus: setOverTimeToggleStatus } = + useQueryToggle(`${QUERY_ID} overTime`); + const { toggleStatus: contributorsToggleStatus, setToggleStatus: setContributorsToggleStatus } = + useQueryToggle(`${QUERY_ID} contributors`); + + const [loading, { data, refetch, inspect }] = useUserRiskScore({ + filterQuery: userName ? buildUserNamesFilter([userName]) : undefined, + onlyLatest: false, + skip: !overTimeToggleStatus && !contributorsToggleStatus, + timerange, + }); + + useQueryInspector({ + queryId: QUERY_ID, + loading, + refetch, + setQuery, + deleteQuery, + inspect, + }); + + const toggleContributorsQuery = useCallback( + (status: boolean) => { + setContributorsToggleStatus(status); + }, + [setContributorsToggleStatus] + ); + + const toggleOverTimeQuery = useCallback( + (status: boolean) => { + setOverTimeToggleStatus(status); + }, + [setOverTimeToggleStatus] + ); + + const rules = data && data.length > 0 ? data[data.length - 1].risk_stats.rule_risks : []; + + return ( + <> + + + + + + + + + + + + {/* // TODO PENDING ON USER RISK DOCUMENTATION + + + {i18n.VIEW_DASHBOARD_BUTTON} + + */} + + + + + + ); +}; + +UserRiskTabBodyComponent.displayName = 'UserRiskTabBodyComponent'; + +export const UserRiskTabBody = React.memo(UserRiskTabBodyComponent); + +UserRiskTabBody.displayName = 'UserRiskTabBody'; diff --git a/x-pack/plugins/security_solution/public/users/pages/translations.ts b/x-pack/plugins/security_solution/public/users/pages/translations.ts index 41fec21c5bfb0..c36abbaab86ec 100644 --- a/x-pack/plugins/security_solution/public/users/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/users/pages/translations.ts @@ -35,7 +35,7 @@ export const NAVIGATION_ANOMALIES_TITLE = i18n.translate( export const NAVIGATION_RISK_TITLE = i18n.translate( 'xpack.securitySolution.users.navigation.riskTitle', { - defaultMessage: 'Users by risk', + defaultMessage: 'User risk', } ); @@ -52,3 +52,17 @@ export const NAVIGATION_ALERTS_TITLE = i18n.translate( defaultMessage: 'External alerts', } ); + +export const USER_RISK_SCORE_OVER_TIME = i18n.translate( + 'xpack.securitySolution.users.navigation.userScoreOverTimeTitle', + { + defaultMessage: 'User risk score over time', + } +); + +export const VIEW_DASHBOARD_BUTTON = i18n.translate( + 'xpack.securitySolution.hosts.navigaton.hostRisk.viewDashboardButtonLabel', + { + defaultMessage: 'View source dashboard', + } +); diff --git a/x-pack/plugins/security_solution/server/features.ts b/x-pack/plugins/security_solution/server/features.ts index 340c4c54c5fc6..375f66a36c3ac 100644 --- a/x-pack/plugins/security_solution/server/features.ts +++ b/x-pack/plugins/security_solution/server/features.ts @@ -133,6 +133,9 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban rule: { all: ruleTypes, }, + alert: { + all: ruleTypes, + }, }, management: { insightsAndAlerting: ['triggersActions'], @@ -156,6 +159,9 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban rule: { read: ruleTypes, }, + alert: { + all: ruleTypes, + }, }, management: { insightsAndAlerting: ['triggersActions'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 8e2f0da4e65c9..9f4670e3c252a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -9,7 +9,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; import { ruleTypeMappings } from '@kbn/securitysolution-rules'; -import { SavedObjectsFindResponse } from '@kbn/core/server'; +import { SavedObjectsFindResponse, SavedObjectsFindResult } from '@kbn/core/server'; import { ActionResult } from '@kbn/actions-plugin/server'; import { @@ -49,6 +49,8 @@ import { } from '../../../../../common/detection_engine/schemas/common'; // eslint-disable-next-line no-restricted-imports import type { LegacyRuleNotificationAlertType } from '../../notifications/legacy_types'; +// eslint-disable-next-line no-restricted-imports +import { LegacyIRuleActionsAttributes } from '../../rule_actions/legacy_types'; import { RuleExecutionSummariesByRuleId } from '../../rule_execution_log'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ @@ -575,7 +577,7 @@ export const getAggregateExecutionEvents = (): GetAggregateRuleExecutionEventsRe timed_out: false, indexing_duration_ms: 7, search_duration_ms: 551, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'succeeded', security_message: 'succeeded', }, @@ -598,7 +600,7 @@ export const getAggregateExecutionEvents = (): GetAggregateRuleExecutionEventsRe timed_out: false, indexing_duration_ms: 0, search_duration_ms: 0, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'partial failure', security_message: 'Check privileges failed to execute ResponseError: index_not_found_exception: [index_not_found_exception] Reason: no such index [broken-index] name: "This Rule Makes Alerts, Actions, AND Moar!" id: "f78f3550-a186-11ec-89a1-0bce95157aba" rule id: "b64b4540-d035-4826-a1e7-f505bf4b9653" execution id: "254d8400-9dc7-43c5-ad4b-227273d1a44b" space ID: "default"', @@ -688,14 +690,20 @@ export const getSignalsMigrationStatusRequest = () => /** * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function */ -export const legacyGetNotificationResult = (): LegacyRuleNotificationAlertType => ({ - id: '200dbf2f-b269-4bf9-aa85-11ba32ba73ba', +export const legacyGetNotificationResult = ({ + id = '456', + ruleId = '123', +}: { + id?: string; + ruleId?: string; +} = {}): LegacyRuleNotificationAlertType => ({ + id, name: 'Notification for Rule Test', tags: [], alertTypeId: 'siem.notifications', consumer: 'siem', params: { - ruleAlertId: '85b64e8a-2e40-4096-86af-5ac172c10825', + ruleAlertId: `${ruleId}`, }, schedule: { interval: '5m', @@ -732,10 +740,357 @@ export const legacyGetNotificationResult = (): LegacyRuleNotificationAlertType = /** * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function */ -export const legacyGetFindNotificationsResultWithSingleHit = - (): FindHit => ({ +export const legacyGetHourlyNotificationResult = ( + id = '456', + ruleId = '123' +): LegacyRuleNotificationAlertType => ({ + id, + name: 'Notification for Rule Test', + tags: [], + alertTypeId: 'siem.notifications', + consumer: 'siem', + params: { + ruleAlertId: `${ruleId}`, + }, + schedule: { + interval: '1h', + }, + enabled: true, + actions: [ + { + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + actionTypeId: '.email', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ], + throttle: null, + notifyWhen: 'onActiveAlert', + apiKey: null, + apiKeyOwner: 'elastic', + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: new Date('2020-03-21T11:15:13.530Z'), + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7', + updatedAt: new Date('2020-03-21T12:37:08.730Z'), + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetDailyNotificationResult = ( + id = '456', + ruleId = '123' +): LegacyRuleNotificationAlertType => ({ + id, + name: 'Notification for Rule Test', + tags: [], + alertTypeId: 'siem.notifications', + consumer: 'siem', + params: { + ruleAlertId: `${ruleId}`, + }, + schedule: { + interval: '1d', + }, + enabled: true, + actions: [ + { + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + actionTypeId: '.email', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ], + throttle: null, + notifyWhen: 'onActiveAlert', + apiKey: null, + apiKeyOwner: 'elastic', + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: new Date('2020-03-21T11:15:13.530Z'), + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7', + updatedAt: new Date('2020-03-21T12:37:08.730Z'), + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetWeeklyNotificationResult = ( + id = '456', + ruleId = '123' +): LegacyRuleNotificationAlertType => ({ + id, + name: 'Notification for Rule Test', + tags: [], + alertTypeId: 'siem.notifications', + consumer: 'siem', + params: { + ruleAlertId: `${ruleId}`, + }, + schedule: { + interval: '7d', + }, + enabled: true, + actions: [ + { + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + actionTypeId: '.email', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ], + throttle: null, + notifyWhen: 'onActiveAlert', + apiKey: null, + apiKeyOwner: 'elastic', + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: new Date('2020-03-21T11:15:13.530Z'), + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: '62b3a130-6b70-11ea-9ce9-6b9818c4cbd7', + updatedAt: new Date('2020-03-21T12:37:08.730Z'), + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetFindNotificationsResultWithSingleHit = ( + ruleId = '123' +): FindHit => ({ + page: 1, + perPage: 1, + total: 1, + data: [legacyGetNotificationResult({ ruleId })], +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetSiemNotificationRuleNoActionsSOResult = ( + ruleId = '123' +): SavedObjectsFindResult => ({ + type: 'siem-detection-engine-rule-actions', + id: 'ID_OF_LEGACY_SIDECAR_NO_ACTIONS', + namespaces: ['default'], + attributes: { + actions: [], + ruleThrottle: 'no_actions', + alertThrottle: null, + }, + references: [{ id: ruleId, type: 'alert', name: 'alert_0' }], + migrationVersion: { + 'siem-detection-engine-rule-actions': '7.11.2', + }, + coreMigrationVersion: '7.15.2', + updated_at: '2022-03-31T19:06:40.473Z', + version: 'WzIzNywxXQ==', + score: 0, +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetSiemNotificationRuleEveryRunSOResult = ( + ruleId = '123' +): SavedObjectsFindResult => ({ + type: 'siem-detection-engine-rule-actions', + id: 'ID_OF_LEGACY_SIDECAR_RULE_RUN_ACTIONS', + namespaces: ['default'], + attributes: { + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + action_type_id: '.email', + }, + ], + ruleThrottle: 'rule', + alertThrottle: null, + }, + references: [{ id: ruleId, type: 'alert', name: 'alert_0' }], + migrationVersion: { + 'siem-detection-engine-rule-actions': '7.11.2', + }, + coreMigrationVersion: '7.15.2', + updated_at: '2022-03-31T19:06:40.473Z', + version: 'WzIzNywxXQ==', + score: 0, +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetSiemNotificationRuleHourlyActionsSOResult = ( + ruleId = '123', + connectorId = '456' +): SavedObjectsFindResult => ({ + type: 'siem-detection-engine-rule-actions', + id: 'ID_OF_LEGACY_SIDECAR_HOURLY_ACTIONS', + namespaces: ['default'], + attributes: { + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + action_type_id: '.email', + }, + ], + ruleThrottle: '1h', + alertThrottle: '1h', + }, + references: [ + { id: ruleId, type: 'alert', name: 'alert_0' }, + { id: connectorId, type: 'action', name: 'action_0' }, + ], + migrationVersion: { + 'siem-detection-engine-rule-actions': '7.11.2', + }, + coreMigrationVersion: '7.15.2', + updated_at: '2022-03-31T19:06:40.473Z', + version: 'WzIzNywxXQ==', + score: 0, +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetSiemNotificationRuleDailyActionsSOResult = ( + ruleId = '123', + connectorId = '456' +): SavedObjectsFindResult => ({ + type: 'siem-detection-engine-rule-actions', + id: 'ID_OF_LEGACY_SIDECAR_DAILY_ACTIONS', + namespaces: ['default'], + attributes: { + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + action_type_id: '.email', + }, + ], + ruleThrottle: '1d', + alertThrottle: '1d', + }, + references: [ + { id: ruleId, type: 'alert', name: 'alert_0' }, + { id: connectorId, type: 'action', name: 'action_0' }, + ], + migrationVersion: { + 'siem-detection-engine-rule-actions': '7.11.2', + }, + coreMigrationVersion: '7.15.2', + updated_at: '2022-03-31T19:06:40.473Z', + version: 'WzIzNywxXQ==', + score: 0, +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetSiemNotificationRuleWeeklyActionsSOResult = ( + ruleId = '123', + connectorId = '456' +): SavedObjectsFindResult => ({ + type: 'siem-detection-engine-rule-actions', + id: 'ID_OF_LEGACY_SIDECAR_WEEKLY_ACTIONS', + namespaces: ['default'], + attributes: { + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + action_type_id: '.email', + }, + ], + ruleThrottle: '7d', + alertThrottle: '7d', + }, + references: [ + { id: ruleId, type: 'alert', name: 'alert_0' }, + { id: connectorId, type: 'action', name: 'action_0' }, + ], + migrationVersion: { + 'siem-detection-engine-rule-actions': '7.11.2', + }, + coreMigrationVersion: '7.15.2', + updated_at: '2022-03-31T19:06:40.473Z', + version: 'WzIzNywxXQ==', + score: 0, +}); + +const getLegacyActionSOs = (ruleId = '123', connectorId = '456') => ({ + none: () => legacyGetSiemNotificationRuleNoActionsSOResult(ruleId), + rule: () => legacyGetSiemNotificationRuleEveryRunSOResult(ruleId), + hourly: () => legacyGetSiemNotificationRuleHourlyActionsSOResult(ruleId, connectorId), + daily: () => legacyGetSiemNotificationRuleDailyActionsSOResult(ruleId, connectorId), + weekly: () => legacyGetSiemNotificationRuleWeeklyActionsSOResult(ruleId, connectorId), +}); + +/** + * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function + */ +export const legacyGetSiemNotificationRuleActionsSOResultWithSingleHit = ( + actionTypes: Array<'none' | 'rule' | 'daily' | 'hourly' | 'weekly'>, + ruleId = '123', + connectorId = '456' +): SavedObjectsFindResponse => { + const actions = getLegacyActionSOs(ruleId, connectorId); + + return { page: 1, - perPage: 1, + per_page: 1, total: 1, - data: [legacyGetNotificationResult()], - }); + saved_objects: actionTypes.map((type) => actions[type]()), + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index 327b8afb46b5d..8db750ba220af 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -21,6 +21,15 @@ import { installPrepackagedTimelines } from '../../../timeline/routes/prepackage // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '@kbn/core/server/elasticsearch/client/mocks'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; +import { legacyMigrate } from '../../rules/utils'; + +jest.mock('../../rules/utils', () => { + const actual = jest.requireActual('../../rules/utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -92,6 +101,8 @@ describe('add_prepackaged_rules_route', () => { errors: [], }); + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); + context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue( elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse()) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index 83a40005d0148..85c324008856c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -13,10 +13,20 @@ import { getFindResultWithSingleHit, getDeleteRequestById, getEmptySavedObjectsResponse, + getRuleMock, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { deleteRulesRoute } from './delete_rules_route'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; +import { legacyMigrate } from '../../rules/utils'; + +jest.mock('../../rules/utils', () => { + const actual = jest.requireActual('../../rules/utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); describe('delete_rules', () => { let server: ReturnType; @@ -29,6 +39,8 @@ describe('delete_rules', () => { clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); + deleteRulesRoute(server.router); }); @@ -54,6 +66,7 @@ describe('delete_rules', () => { test('returns 404 when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + (legacyMigrate as jest.Mock).mockResolvedValue(null); const response = await server.inject( getDeleteRequest(), requestContextMock.convertContext(context) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts index 37b1f41328674..c59a0e4dfe176 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts @@ -12,8 +12,6 @@ import { } from '../__mocks__/request_responses'; import { getRuleExecutionEventsRoute } from './get_rule_execution_events_route'; -// TODO: Add additional tests for param validation - describe('getRuleExecutionEventsRoute', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index 6abac6e946380..fa75be49a61c5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -23,9 +23,18 @@ import { patchRulesBulkRoute } from './patch_rules_bulk_route'; import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { legacyMigrate } from '../../rules/utils'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); +jest.mock('../../rules/utils', () => { + const actual = jest.requireActual('../../rules/utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); + describe('patch_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); @@ -40,6 +49,8 @@ describe('patch_rules_bulk', () => { clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); // update succeeds + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); + patchRulesBulkRoute(server.router, ml, logger); }); @@ -54,6 +65,7 @@ describe('patch_rules_bulk', () => { test('returns an error in the response when updating a single rule that does not exist', async () => { clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + (legacyMigrate as jest.Mock).mockResolvedValue(null); const response = await server.inject( getPatchBulkRequest(), requestContextMock.convertContext(context) @@ -148,6 +160,8 @@ describe('patch_rules_bulk', () => { describe('request validation', () => { test('rejects payloads with no ID', async () => { + (legacyMigrate as jest.Mock).mockResolvedValue(null); + const request = requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_BULK_UPDATE, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index cbcf5540b4f15..87c2e79922457 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -21,9 +21,18 @@ import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { patchRulesRoute } from './patch_rules_route'; import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; +import { legacyMigrate } from '../../rules/utils'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); +jest.mock('../../rules/utils', () => { + const actual = jest.requireActual('../../rules/utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); + describe('patch_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); @@ -41,6 +50,8 @@ describe('patch_rules', () => { getRuleExecutionSummarySucceeded() ); + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); + patchRulesRoute(server.router, ml); }); @@ -55,6 +66,7 @@ describe('patch_rules', () => { test('returns 404 when updating a single rule that does not exist', async () => { clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + (legacyMigrate as jest.Mock).mockResolvedValue(null); const response = await server.inject( getPatchRequest(), requestContextMock.convertContext(context) @@ -67,6 +79,7 @@ describe('patch_rules', () => { }); test('returns error if requesting a non-rule', async () => { + (legacyMigrate as jest.Mock).mockResolvedValue(null); clients.rulesClient.find.mockResolvedValue(nonRuleFindResult()); const response = await server.inject( getPatchRequest(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts index 3dc8ab5199c7f..4c50ee58e2d7f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts @@ -14,6 +14,7 @@ import { getBulkActionEditRequest, getFindResultWithSingleHit, getFindResultWithMultiHits, + getRuleMock, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { performBulkActionRoute } from './perform_bulk_action_route'; @@ -23,10 +24,20 @@ import { } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock'; import { loggingSystemMock } from '@kbn/core/server/mocks'; import { readRules } from '../../rules/read_rules'; +import { legacyMigrate } from '../../rules/utils'; +import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); jest.mock('../../rules/read_rules', () => ({ readRules: jest.fn() })); +jest.mock('../../rules/utils', () => { + const actual = jest.requireActual('../../rules/utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); + describe('perform_bulk_action', () => { const readRulesMock = readRules as jest.Mock; let server: ReturnType; @@ -40,6 +51,8 @@ describe('perform_bulk_action', () => { logger = loggingSystemMock.createLogger(); ({ clients, context } = requestContextMock.createTools()); ml = mlServicesMock.createSetupContract(); + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); + clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); performBulkActionRoute(server.router, ml, logger); }); @@ -220,7 +233,10 @@ describe('perform_bulk_action', () => { readRulesMock.mockImplementationOnce(() => Promise.resolve({ ...mockRule, params: { ...mockRule.params, type: 'machine_learning' } }) ); - + (legacyMigrate as jest.Mock).mockResolvedValue({ + ...mockRule, + params: { ...mockRule.params, type: 'machine_learning' }, + }); const request = requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_BULK_ACTION, @@ -271,7 +287,10 @@ describe('perform_bulk_action', () => { readRulesMock.mockImplementationOnce(() => Promise.resolve({ ...mockRule, params: { ...mockRule.params, index: ['index-*'] } }) ); - + (legacyMigrate as jest.Mock).mockResolvedValue({ + ...mockRule, + params: { ...mockRule.params, index: ['index-*'] }, + }); const request = requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_BULK_ACTION, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 88720646fa6cd..e0c9289a562e7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -21,9 +21,18 @@ import { BulkError } from '../utils'; import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { legacyMigrate } from '../../rules/utils'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); +jest.mock('../../rules/utils', () => { + const actual = jest.requireActual('../../rules/utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); + describe('update_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); @@ -40,6 +49,8 @@ describe('update_rules_bulk', () => { clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index'); + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); + updateRulesBulkRoute(server.router, ml, logger); }); @@ -54,6 +65,8 @@ describe('update_rules_bulk', () => { test('returns 200 as a response when updating a single rule that does not exist', async () => { clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + (legacyMigrate as jest.Mock).mockResolvedValue(null); + const expected: BulkError[] = [ { error: { message: 'rule_id: "rule-1" not found', status_code: 404 }, @@ -116,6 +129,8 @@ describe('update_rules_bulk', () => { describe('request validation', () => { test('rejects payloads with no ID', async () => { + (legacyMigrate as jest.Mock).mockResolvedValue(null); + const noIdRequest = requestMock.create({ method: 'put', path: DETECTION_ENGINE_RULES_BULK_UPDATE, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 39040f4c9c4fc..7f2d5c3bde7e0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -21,9 +21,18 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { updateRulesRoute } from './update_rules_route'; import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; +import { legacyMigrate } from '../../rules/utils'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); +jest.mock('../../rules/utils', () => { + const actual = jest.requireActual('../../rules/utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); + describe('update_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); @@ -42,6 +51,8 @@ describe('update_rules', () => { ); clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index'); + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); + updateRulesRoute(server.router, ml); }); @@ -56,6 +67,7 @@ describe('update_rules', () => { test('returns 404 when updating a single rule that does not exist', async () => { clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + (legacyMigrate as jest.Mock).mockResolvedValue(null); const response = await server.inject( getUpdateRequest(), requestContextMock.convertContext(context) @@ -69,6 +81,7 @@ describe('update_rules', () => { }); test('returns error when updating non-rule', async () => { + (legacyMigrate as jest.Mock).mockResolvedValue(null); clients.rulesClient.find.mockResolvedValue(nonRuleFindResult()); const response = await server.inject( getUpdateRequest(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_routes/client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_routes/client_interface.ts index 8d0cae91f1987..3f353732abbc5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_routes/client_interface.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_routes/client_interface.ts @@ -9,7 +9,7 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ExecutionLogTableSortColumns, RuleExecutionEvent, - RuleExecutionStatusType, + RuleExecutionStatus, RuleExecutionSummary, } from '../../../../../common/detection_engine/schemas/common'; import { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response'; @@ -19,7 +19,7 @@ export interface GetAggregateExecutionEventsArgs { start: string; end: string; queryText: string; - statusFilters: RuleExecutionStatusType[]; + statusFilters: RuleExecutionStatus[]; page: number; perPage: number; sortField: ExecutionLogTableSortColumns; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/event_log_reader.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/event_log_reader.ts index 78e8e62702d47..20cc2f0e78c77 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/event_log_reader.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/event_log_reader.ts @@ -6,8 +6,8 @@ */ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; import { IEventLogClient } from '@kbn/event-log-plugin/server'; +import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; import { RuleExecutionEvent, @@ -18,13 +18,14 @@ import { invariant } from '../../../../../common/utils/invariant'; import { withSecuritySpan } from '../../../../utils/with_security_span'; import { GetAggregateExecutionEventsArgs } from '../client_for_routes/client_interface'; import { - RULE_SAVED_OBJECT_TYPE, RULE_EXECUTION_LOG_PROVIDER, + RULE_SAVED_OBJECT_TYPE, RuleExecutionLogAction, } from './constants'; import { formatExecutionEventResponse, getExecutionEventAggregation, + mapRuleExecutionStatusToPlatformStatus, } from './get_execution_event_aggregation'; import { EXECUTION_UUID_FIELD, @@ -62,10 +63,15 @@ export const createEventLogReader = (eventLog: IEventLogClient): IEventLogReader let totalExecutions: number | undefined; // If 0 or 3 statuses are selected we can search for all statuses and don't need this pre-filter by ID if (statusFilters.length > 0 && statusFilters.length < 3) { + const outcomes = mapRuleExecutionStatusToPlatformStatus(statusFilters); + const outcomeFilter = outcomes.length ? `OR event.outcome:(${outcomes.join(' OR ')})` : ''; const statusResults = await eventLog.aggregateEventsBySavedObjectIds(soType, soIds, { start, end, - filter: `kibana.alert.rule.execution.status:(${statusFilters.join(' OR ')})`, + // Also query for `event.outcome` to catch executions that only contain platform events + filter: `kibana.alert.rule.execution.status:(${statusFilters.join( + ' OR ' + )}) ${outcomeFilter}`, aggs: { totalExecutions: { cardinality: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.test.ts index 7cee84cd64594..dcd592d7a70fc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.test.ts @@ -12,6 +12,7 @@ * 2.0. */ +import { RuleExecutionStatus } from '../../../../../../common/detection_engine/schemas/common'; import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; import { @@ -20,6 +21,8 @@ import { formatSortForTermsSort, getExecutionEventAggregation, getProviderAndActionFilter, + mapPlatformStatusToRuleExecutionStatus, + mapRuleExecutionStatusToPlatformStatus, } from '.'; describe('getExecutionEventAggregation', () => { @@ -69,7 +72,7 @@ describe('getExecutionEventAggregation', () => { sort: [{ notsortable: { order: 'asc' } }], }); }).toThrowErrorMatchingInlineSnapshot( - `"Invalid sort field \\"notsortable\\" - must be one of [timestamp,duration_ms,indexing_duration_ms,search_duration_ms,gap_duration_ms,schedule_delay_ms,num_triggered_actions]"` + `"Invalid sort field \\"notsortable\\" - must be one of [timestamp,duration_ms,indexing_duration_ms,search_duration_ms,gap_duration_s,schedule_delay_ms,num_triggered_actions]"` ); }); @@ -82,7 +85,7 @@ describe('getExecutionEventAggregation', () => { sort: [{ notsortable: { order: 'asc' } }, { timestamp: { order: 'asc' } }], }); }).toThrowErrorMatchingInlineSnapshot( - `"Invalid sort field \\"notsortable\\" - must be one of [timestamp,duration_ms,indexing_duration_ms,search_duration_ms,gap_duration_ms,schedule_delay_ms,num_triggered_actions]"` + `"Invalid sort field \\"notsortable\\" - must be one of [timestamp,duration_ms,indexing_duration_ms,search_duration_ms,gap_duration_s,schedule_delay_ms,num_triggered_actions]"` ); }); @@ -206,7 +209,7 @@ describe('getExecutionEventAggregation', () => { top_hits: { size: 1, _source: { - includes: ['event.outcome', 'message'], + includes: ['error.message', 'event.outcome', 'message'], }, }, }, @@ -647,7 +650,7 @@ describe('formatExecutionEventResponse', () => { timed_out: false, indexing_duration_ms: 7, search_duration_ms: 480, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'succeeded', security_message: 'succeeded', }, @@ -670,7 +673,7 @@ describe('formatExecutionEventResponse', () => { timed_out: false, indexing_duration_ms: 0, search_duration_ms: 9, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'succeeded', security_message: 'succeeded', }, @@ -965,7 +968,7 @@ describe('formatExecutionEventResponse', () => { timed_out: true, indexing_duration_ms: 7, search_duration_ms: 480, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'succeeded', security_message: 'succeeded', }, @@ -988,7 +991,7 @@ describe('formatExecutionEventResponse', () => { timed_out: false, indexing_duration_ms: 0, search_duration_ms: 9, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'succeeded', security_message: 'succeeded', }, @@ -1288,7 +1291,7 @@ describe('formatExecutionEventResponse', () => { timed_out: true, indexing_duration_ms: 7, search_duration_ms: 480, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'succeeded', security_message: 'succeeded', }, @@ -1311,7 +1314,7 @@ describe('formatExecutionEventResponse', () => { timed_out: false, indexing_duration_ms: 0, search_duration_ms: 9, - gap_duration_ms: 0, + gap_duration_s: 0, security_status: 'succeeded', security_message: 'succeeded', }, @@ -1319,3 +1322,53 @@ describe('formatExecutionEventResponse', () => { }); }); }); + +describe('mapRuleStatusToPlatformStatus', () => { + test('should correctly translate empty array to empty array', () => { + expect(mapRuleExecutionStatusToPlatformStatus([])).toEqual([]); + }); + + test('should correctly translate RuleExecutionStatus.failed to `failure` platform status', () => { + expect(mapRuleExecutionStatusToPlatformStatus([RuleExecutionStatus.failed])).toEqual([ + 'failure', + ]); + }); + + test('should correctly translate RuleExecutionStatus.succeeded to `success` platform status', () => { + expect(mapRuleExecutionStatusToPlatformStatus([RuleExecutionStatus.succeeded])).toEqual([ + 'success', + ]); + }); + + test('should correctly translate RuleExecutionStatus.["going to run"] to empty array platform status', () => { + expect(mapRuleExecutionStatusToPlatformStatus([RuleExecutionStatus['going to run']])).toEqual( + [] + ); + }); + + test("should correctly translate multiple RuleExecutionStatus's to platform statuses", () => { + expect( + mapRuleExecutionStatusToPlatformStatus([ + RuleExecutionStatus.succeeded, + RuleExecutionStatus.failed, + RuleExecutionStatus['going to run'], + ]).sort() + ).toEqual(['failure', 'success']); + }); +}); + +describe('mapPlatformStatusToRuleExecutionStatus', () => { + test('should correctly translate `invalid` platform status to `undefined`', () => { + expect(mapPlatformStatusToRuleExecutionStatus('')).toEqual(undefined); + }); + + test('should correctly translate `failure` platform status to `RuleExecutionStatus.failed`', () => { + expect(mapPlatformStatusToRuleExecutionStatus('failure')).toEqual(RuleExecutionStatus.failed); + }); + + test('should correctly translate `success` platform status to `RuleExecutionStatus.succeeded`', () => { + expect(mapPlatformStatusToRuleExecutionStatus('success')).toEqual( + RuleExecutionStatus.succeeded + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.ts index 4cf8e0bd5f1dc..dcf2fbfe911bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.ts @@ -10,7 +10,10 @@ import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { flatMap, get } from 'lodash'; import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; import { AggregateEventsBySavedObjectResult } from '@kbn/event-log-plugin/server'; -import { AggregateRuleExecutionEvent } from '../../../../../../common/detection_engine/schemas/common'; +import { + AggregateRuleExecutionEvent, + RuleExecutionStatus, +} from '../../../../../../common/detection_engine/schemas/common'; import { GetAggregateRuleExecutionEventsResponse } from '../../../../../../common/detection_engine/schemas/response'; import { ExecutionEventAggregationOptions, @@ -22,6 +25,7 @@ import { // Base ECS fields const ACTION_FIELD = 'event.action'; const DURATION_FIELD = 'event.duration'; +const ERROR_MESSAGE_FIELD = 'error.message'; const MESSAGE_FIELD = 'message'; const PROVIDER_FIELD = 'event.provider'; const OUTCOME_FIELD = 'event.outcome'; @@ -48,7 +52,7 @@ const SORT_FIELD_TO_AGG_MAPPING: Record = { duration_ms: 'ruleExecution>executionDuration', indexing_duration_ms: 'securityMetrics>indexDuration', search_duration_ms: 'securityMetrics>searchDuration', - gap_duration_ms: 'securityMetrics>gapDuration', + gap_duration_s: 'securityMetrics>gapDuration', schedule_delay_ms: 'ruleExecution>scheduleDelay', num_triggered_actions: 'ruleExecution>numTriggeredActions', // TODO: To be added in https://github.com/elastic/kibana/pull/126210 @@ -166,7 +170,7 @@ export const getExecutionEventAggregation = ({ top_hits: { size: 1, _source: { - includes: [OUTCOME_FIELD, MESSAGE_FIELD], + includes: [ERROR_MESSAGE_FIELD, OUTCOME_FIELD, MESSAGE_FIELD], }, }, }, @@ -293,11 +297,18 @@ export const formatAggExecutionEventFromBucket = ( // security fields indexing_duration_ms: bucket?.securityMetrics?.indexDuration?.value ?? 0, search_duration_ms: bucket?.securityMetrics?.searchDuration?.value ?? 0, - gap_duration_ms: bucket?.securityMetrics?.gapDuration?.value ?? 0, + gap_duration_s: bucket?.securityMetrics?.gapDuration?.value ?? 0, + // If security_status isn't available, use platform status from `event.outcome`, but translate to RuleExecutionStatus security_status: bucket?.securityStatus?.status?.hits?.hits[0]?._source?.kibana?.alert?.rule?.execution - ?.status, - security_message: bucket?.securityStatus?.message?.hits?.hits[0]?._source?.message, + ?.status ?? + mapPlatformStatusToRuleExecutionStatus( + bucket?.ruleExecution?.outcomeAndMessage?.hits?.hits[0]?._source?.event?.outcome + ), + // If security_message isn't available, use `error.message` instead for platform errors since it is more descriptive than `message` + security_message: + bucket?.securityStatus?.message?.hits?.hits[0]?._source?.message ?? + bucket?.ruleExecution?.outcomeAndMessage?.hits?.hits[0]?._source?.error?.message, }; }; @@ -353,3 +364,40 @@ export const formatSortForTermsSort = (sort: estypes.Sort) => { ) ); }; + +/** + * Maps a RuleExecutionStatus[] to string[] of associated platform statuses. Useful for querying specific platform + * events based on security status values + * @param ruleStatuses RuleExecutionStatus[] + */ +export const mapRuleExecutionStatusToPlatformStatus = ( + ruleStatuses: RuleExecutionStatus[] +): string[] => { + return flatMap(ruleStatuses, (rs) => { + switch (rs) { + case RuleExecutionStatus.failed: + return 'failure'; + case RuleExecutionStatus.succeeded: + return 'success'; + default: + return []; + } + }); +}; + +/** + * Maps a platform status string to RuleExecutionStatus + * @param platformStatus string, i.e. `failure` or `success` + */ +export const mapPlatformStatusToRuleExecutionStatus = ( + platformStatus: string +): RuleExecutionStatus | undefined => { + switch (platformStatus) { + case 'failure': + return RuleExecutionStatus.failed; + case 'success': + return RuleExecutionStatus.succeeded; + default: + return undefined; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts index 96fba6703a537..2a54ce1e976d4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts @@ -7,14 +7,24 @@ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; import { savedObjectsClientMock } from '@kbn/core/server/mocks'; -import { getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; +import { getRuleMock, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; import { updatePrepackagedRules } from './update_prepacked_rules'; import { patchRules } from './patch_rules'; import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock'; import { ruleExecutionLogMock } from '../rule_execution_log/__mocks__'; +import { legacyMigrate } from './utils'; +import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; jest.mock('./patch_rules'); +jest.mock('./utils', () => { + const actual = jest.requireActual('./utils'); + return { + ...actual, + legacyMigrate: jest.fn(), + }; +}); + describe('updatePrepackagedRules', () => { let rulesClient: ReturnType; let savedObjectsClient: ReturnType; @@ -24,6 +34,8 @@ describe('updatePrepackagedRules', () => { rulesClient = rulesClientMock.create(); savedObjectsClient = savedObjectsClientMock.create(); ruleExecutionLog = ruleExecutionLogMock.forRoutes.create(); + + (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); }); it('should omit actions and enabled when calling patchRules', async () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts index 5151a58d0d885..0952da3182e01 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts @@ -13,6 +13,8 @@ import { transformToAlertThrottle, transformFromAlertThrottle, transformActions, + legacyMigrate, + getUpdatedActionsParams, } from './utils'; import { RuleAction, SanitizedRule } from '@kbn/alerting-plugin/common'; import { RuleParams } from '../schemas/rule_schemas'; @@ -23,6 +25,67 @@ import { import { FullResponseSchema } from '../../../../common/detection_engine/schemas/request'; // eslint-disable-next-line no-restricted-imports import { LegacyRuleActions } from '../rule_actions/legacy_types'; +import { + getEmptyFindResult, + legacyGetSiemNotificationRuleActionsSOResultWithSingleHit, + legacyGetDailyNotificationResult, + legacyGetHourlyNotificationResult, + legacyGetWeeklyNotificationResult, +} from '../routes/__mocks__/request_responses'; +import { requestContextMock } from '../routes/__mocks__'; + +const getRuleLegacyActions = (): SanitizedRule => + ({ + id: '123', + notifyWhen: 'onThrottleInterval', + name: 'Simple Rule Query', + tags: ['__internal_rule_id:ruleId', '__internal_immutable:false'], + alertTypeId: 'siem.queryRule', + consumer: 'siem', + enabled: true, + throttle: '1h', + apiKeyOwner: 'elastic', + createdBy: 'elastic', + updatedBy: 'elastic', + muteAll: false, + mutedInstanceIds: [], + monitoring: { execution: { history: [], calculated_metrics: { success_ratio: 0 } } }, + mapped_params: { risk_score: 1, severity: '60-high' }, + schedule: { interval: '5m' }, + actions: [], + params: { + author: [], + description: 'Simple Rule Query', + ruleId: 'ruleId', + falsePositives: [], + from: 'now-6m', + immutable: false, + outputIndex: '.siem-signals-default', + maxSignals: 100, + riskScore: 1, + riskScoreMapping: [], + severity: 'high', + severityMapping: [], + threat: [], + to: 'now', + references: [], + version: 1, + exceptionsList: [], + type: 'query', + language: 'kuery', + index: ['auditbeat-*'], + query: 'user.name: root or user.name: admin', + }, + snoozeEndTime: null, + updatedAt: '2022-03-31T21:47:25.695Z', + createdAt: '2022-03-31T21:47:16.379Z', + scheduledTaskId: '21bb9b60-b13c-11ec-99d0-asdfasdfasf', + executionStatus: { + status: 'pending', + lastExecutionDate: '2022-03-31T21:47:25.695Z', + lastDuration: 0, + }, + } as unknown as SanitizedRule); describe('utils', () => { describe('#calculateInterval', () => { @@ -588,4 +651,415 @@ describe('utils', () => { ]); }); }); + + describe('#legacyMigrate', () => { + const ruleId = '123'; + const connectorId = '456'; + const { clients } = requestContextMock.createTools(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('it does no cleanup or migration if no legacy reminants found', async () => { + clients.rulesClient.find.mockResolvedValueOnce(getEmptyFindResult()); + clients.savedObjectsClient.find.mockResolvedValueOnce({ + page: 0, + per_page: 0, + total: 0, + saved_objects: [], + }); + + const rule = { + ...getRuleLegacyActions(), + id: ruleId, + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + muteAll: true, + } as SanitizedRule; + + const migratedRule = await legacyMigrate({ + rulesClient: clients.rulesClient, + savedObjectsClient: clients.savedObjectsClient, + rule, + }); + + expect(clients.rulesClient.delete).not.toHaveBeenCalled(); + expect(clients.savedObjectsClient.delete).not.toHaveBeenCalled(); + expect(migratedRule).toEqual(rule); + }); + + // Even if a rule is created with no actions pre 7.16, a + // siem-detection-engine-rule-actions SO is still created + test('it migrates a rule with no actions', async () => { + // siem.notifications is not created for a rule with no actions + clients.rulesClient.find.mockResolvedValueOnce(getEmptyFindResult()); + // siem-detection-engine-rule-actions SO is still created + clients.savedObjectsClient.find.mockResolvedValueOnce( + legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['none'], ruleId, connectorId) + ); + + const migratedRule = await legacyMigrate({ + rulesClient: clients.rulesClient, + savedObjectsClient: clients.savedObjectsClient, + rule: { + ...getRuleLegacyActions(), + id: ruleId, + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + muteAll: true, + }, + }); + + expect(clients.rulesClient.delete).not.toHaveBeenCalled(); + expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith( + 'siem-detection-engine-rule-actions', + 'ID_OF_LEGACY_SIDECAR_NO_ACTIONS' + ); + expect(migratedRule?.actions).toEqual([]); + expect(migratedRule?.throttle).toBeNull(); + expect(migratedRule?.muteAll).toBeTruthy(); + expect(migratedRule?.notifyWhen).toEqual('onActiveAlert'); + }); + + test('it migrates a rule with every rule run action', async () => { + // siem.notifications is not created for a rule with actions run every rule run + clients.rulesClient.find.mockResolvedValueOnce(getEmptyFindResult()); + // siem-detection-engine-rule-actions SO is still created + clients.savedObjectsClient.find.mockResolvedValueOnce( + legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['rule'], ruleId, connectorId) + ); + + const migratedRule = await legacyMigrate({ + rulesClient: clients.rulesClient, + savedObjectsClient: clients.savedObjectsClient, + rule: { + ...getRuleLegacyActions(), + id: ruleId, + actions: [ + { + actionTypeId: '.email', + params: { + subject: 'Test Actions', + to: ['test@test.com'], + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + id: connectorId, + group: 'default', + }, + ], + throttle: null, + notifyWhen: 'onActiveAlert', + muteAll: false, + }, + }); + + expect(clients.rulesClient.delete).not.toHaveBeenCalled(); + expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith( + 'siem-detection-engine-rule-actions', + 'ID_OF_LEGACY_SIDECAR_RULE_RUN_ACTIONS' + ); + expect(migratedRule?.actions).toEqual([ + { + id: connectorId, + actionTypeId: '.email', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + subject: 'Test Actions', + to: ['test@test.com'], + }, + }, + ]); + expect(migratedRule?.notifyWhen).toEqual('onActiveAlert'); + expect(migratedRule?.throttle).toBeNull(); + expect(migratedRule?.muteAll).toBeFalsy(); + }); + + test('it migrates a rule with daily legacy actions', async () => { + // siem.notifications is not created for a rule with no actions + clients.rulesClient.find.mockResolvedValueOnce({ + page: 1, + perPage: 1, + total: 1, + data: [legacyGetDailyNotificationResult(connectorId, ruleId)], + }); + // siem-detection-engine-rule-actions SO is still created + clients.savedObjectsClient.find.mockResolvedValueOnce( + legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['daily'], ruleId, connectorId) + ); + + const migratedRule = await legacyMigrate({ + rulesClient: clients.rulesClient, + savedObjectsClient: clients.savedObjectsClient, + rule: { + ...getRuleLegacyActions(), + id: ruleId, + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }); + + expect(clients.rulesClient.delete).toHaveBeenCalledWith({ id: '456' }); + expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith( + 'siem-detection-engine-rule-actions', + 'ID_OF_LEGACY_SIDECAR_DAILY_ACTIONS' + ); + expect(migratedRule?.actions).toEqual([ + { + actionTypeId: '.email', + group: 'default', + id: connectorId, + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + }, + ]); + expect(migratedRule?.throttle).toEqual('1d'); + expect(migratedRule?.notifyWhen).toEqual('onThrottleInterval'); + expect(migratedRule?.muteAll).toBeFalsy(); + }); + + test('it migrates a rule with hourly legacy actions', async () => { + // siem.notifications is not created for a rule with no actions + clients.rulesClient.find.mockResolvedValueOnce({ + page: 1, + perPage: 1, + total: 1, + data: [legacyGetHourlyNotificationResult(connectorId, ruleId)], + }); + // siem-detection-engine-rule-actions SO is still created + clients.savedObjectsClient.find.mockResolvedValueOnce( + legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['hourly'], ruleId, connectorId) + ); + + const migratedRule = await legacyMigrate({ + rulesClient: clients.rulesClient, + savedObjectsClient: clients.savedObjectsClient, + rule: { + ...getRuleLegacyActions(), + id: ruleId, + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }); + + expect(clients.rulesClient.delete).toHaveBeenCalledWith({ id: '456' }); + expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith( + 'siem-detection-engine-rule-actions', + 'ID_OF_LEGACY_SIDECAR_HOURLY_ACTIONS' + ); + expect(migratedRule?.actions).toEqual([ + { + actionTypeId: '.email', + group: 'default', + id: connectorId, + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + }, + ]); + expect(migratedRule?.throttle).toEqual('1h'); + expect(migratedRule?.notifyWhen).toEqual('onThrottleInterval'); + expect(migratedRule?.muteAll).toBeFalsy(); + }); + + test('it migrates a rule with weekly legacy actions', async () => { + // siem.notifications is not created for a rule with no actions + clients.rulesClient.find.mockResolvedValueOnce({ + page: 1, + perPage: 1, + total: 1, + data: [legacyGetWeeklyNotificationResult(connectorId, ruleId)], + }); + // siem-detection-engine-rule-actions SO is still created + clients.savedObjectsClient.find.mockResolvedValueOnce( + legacyGetSiemNotificationRuleActionsSOResultWithSingleHit(['weekly'], ruleId, connectorId) + ); + + const migratedRule = await legacyMigrate({ + rulesClient: clients.rulesClient, + savedObjectsClient: clients.savedObjectsClient, + rule: { + ...getRuleLegacyActions(), + id: ruleId, + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }); + + expect(clients.rulesClient.delete).toHaveBeenCalledWith({ id: '456' }); + expect(clients.savedObjectsClient.delete).toHaveBeenCalledWith( + 'siem-detection-engine-rule-actions', + 'ID_OF_LEGACY_SIDECAR_WEEKLY_ACTIONS' + ); + expect(migratedRule?.actions).toEqual([ + { + actionTypeId: '.email', + group: 'default', + id: connectorId, + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Test Actions', + }, + }, + ]); + expect(migratedRule?.throttle).toEqual('7d'); + expect(migratedRule?.notifyWhen).toEqual('onThrottleInterval'); + expect(migratedRule?.muteAll).toBeFalsy(); + }); + }); + + describe('#getUpdatedActionsParams', () => { + it('updates one action', () => { + const { id, ...rule } = { + ...getRuleLegacyActions(), + id: '123', + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + } as SanitizedRule; + + expect( + getUpdatedActionsParams({ + rule: { + ...rule, + id, + }, + ruleThrottle: '1h', + actions: [ + { + actionRef: 'action_0', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['a@a.com'], + subject: 'Test Actions', + }, + action_type_id: '.email', + }, + ], + references: [ + { + id: '61ec7a40-b076-11ec-bb3f-1f063f8e06cf', + type: 'alert', + name: 'alert_0', + }, + { + id: '1234', + type: 'action', + name: 'action_0', + }, + ], + }) + ).toEqual({ + ...rule, + actions: [ + { + actionTypeId: '.email', + group: 'default', + id: '1234', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + subject: 'Test Actions', + to: ['a@a.com'], + }, + }, + ], + throttle: '1h', + notifyWhen: 'onThrottleInterval', + }); + }); + + it('updates multiple actions', () => { + const { id, ...rule } = { + ...getRuleLegacyActions(), + id: '123', + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + } as SanitizedRule; + + expect( + getUpdatedActionsParams({ + rule: { + ...rule, + id, + }, + ruleThrottle: '1h', + actions: [ + { + actionRef: 'action_0', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + to: ['test@test.com'], + subject: 'Rule email', + }, + action_type_id: '.email', + }, + { + actionRef: 'action_1', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + action_type_id: '.slack', + }, + ], + references: [ + { + id: '064e3160-b076-11ec-bb3f-1f063f8e06cf', + type: 'alert', + name: 'alert_0', + }, + { + id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', + type: 'action', + name: 'action_0', + }, + { + id: '207fa0e0-c04e-11ec-8a52-4fb92379525a', + type: 'action', + name: 'action_1', + }, + ], + }) + ).toEqual({ + ...rule, + actions: [ + { + actionTypeId: '.email', + group: 'default', + id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + subject: 'Rule email', + to: ['test@test.com'], + }, + }, + { + actionTypeId: '.slack', + group: 'default', + id: '207fa0e0-c04e-11ec-8a52-4fb92379525a', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + }, + ], + throttle: '1h', + notifyWhen: 'onThrottleInterval', + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts index c7858a131cb1f..dd25676a758e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts @@ -28,6 +28,7 @@ import type { } from '@kbn/securitysolution-io-ts-alerting-types'; import type { ListArrayOrUndefined } from '@kbn/securitysolution-io-ts-list-types'; import type { VersionOrUndefined } from '@kbn/securitysolution-io-ts-types'; +import { SavedObjectReference } from '@kbn/core/server'; import { RuleAction, RuleNotifyWhenType, SanitizedRule } from '@kbn/alerting-plugin/common'; import { RulesClient } from '@kbn/alerting-plugin/server'; import { @@ -63,7 +64,11 @@ import { NOTIFICATION_THROTTLE_RULE, } from '../../../../common/constants'; // eslint-disable-next-line no-restricted-imports -import { LegacyRuleActions } from '../rule_actions/legacy_types'; +import { + LegacyIRuleActionsAttributes, + LegacyRuleActions, + LegacyRuleAlertSavedObjectAction, +} from '../rule_actions/legacy_types'; import { FullResponseSchema } from '../../../../common/detection_engine/schemas/request'; import { transformAlertToRuleAction } from '../../../../common/detection_engine/transform_actions'; // eslint-disable-next-line no-restricted-imports @@ -302,6 +307,59 @@ export const maybeMute = async ({ } }; +/** + * Translate legacy action sidecar action to rule action + */ +export const getUpdatedActionsParams = ({ + rule, + ruleThrottle, + actions, + references, +}: { + rule: SanitizedRule; + ruleThrottle: string | null; + actions: LegacyRuleAlertSavedObjectAction[]; + references: SavedObjectReference[]; +}): Omit, 'id'> => { + const { id, ...restOfRule } = rule; + + const actionReference = references.reduce>( + (acc, reference) => { + acc[reference.name] = reference; + return acc; + }, + {} + ); + + if (isEmpty(actionReference)) { + throw new Error( + `An error occurred migrating legacy action for rule with id:${id}. Connector reference id not found.` + ); + } + // If rule has an action on any other interval (other than on every + // rule run), need to move the action info from the sidecar/legacy action + // into the rule itself + return { + ...restOfRule, + actions: actions.reduce((acc, action) => { + const { actionRef, action_type_id: actionTypeId, ...resOfAction } = action; + if (!actionReference[actionRef]) { + return acc; + } + return [ + ...acc, + { + ...resOfAction, + id: actionReference[actionRef].id, + actionTypeId, + }, + ]; + }, []), + throttle: transformToAlertThrottle(ruleThrottle), + notifyWhen: transformToNotifyWhen(ruleThrottle), + }; +}; + /** * Determines if rule needs to be migrated from legacy actions * and returns necessary pieces for the updated rule @@ -332,7 +390,7 @@ export const legacyMigrate = async ({ }, }, }), - savedObjectsClient.find({ + savedObjectsClient.find({ type: legacyRuleActionsSavedObjectType, hasReference: { type: 'alert', @@ -341,29 +399,57 @@ export const legacyMigrate = async ({ }), ]); - if (siemNotification != null && siemNotification.data.length > 0) { - await Promise.all([ - rulesClient.delete({ id: siemNotification.data[0].id }), - legacyRuleActionsSO != null && legacyRuleActionsSO.saved_objects.length > 0 - ? savedObjectsClient.delete( - legacyRuleActionsSavedObjectType, - legacyRuleActionsSO.saved_objects[0].id - ) - : null, - ]); + const siemNotificationsExist = siemNotification != null && siemNotification.data.length > 0; + const legacyRuleNotificationSOsExist = + legacyRuleActionsSO != null && legacyRuleActionsSO.saved_objects.length > 0; + + // Assumption: if no legacy sidecar SO or notification rule types exist + // that reference the rule in question, assume rule actions are not legacy + if (!siemNotificationsExist && !legacyRuleNotificationSOsExist) { + return rule; + } + // If the legacy notification rule type ("siem.notification") exist, + // migration and cleanup are needed + if (siemNotificationsExist) { + await rulesClient.delete({ id: siemNotification.data[0].id }); + } + // If legacy notification sidecar ("siem-detection-engine-rule-actions") + // exist, migration and cleanup are needed + if (legacyRuleNotificationSOsExist) { + // Delete the legacy sidecar SO + await savedObjectsClient.delete( + legacyRuleActionsSavedObjectType, + legacyRuleActionsSO.saved_objects[0].id + ); + + // If "siem-detection-engine-rule-actions" notes that `ruleThrottle` is + // "no_actions" or "rule", rule has no actions or rule is set to run + // action on every rule run. In these cases, sidecar deletion is the only + // cleanup needed and updates to the "throttle" and "notifyWhen". "siem.notification" are + // not created for these action types + if ( + legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle === 'no_actions' || + legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle === 'rule' + ) { + return rule; + } + + // Use "legacyRuleActionsSO" instead of "siemNotification" as "siemNotification" is not created + // until a rule is run and added to task manager. That means that if by chance a user has a rule + // with actions which they have yet to enable, the actions would be lost. Instead, + // "legacyRuleActionsSO" is created on rule creation (pre 7.15) and we can rely on it to be there + const migratedRule = getUpdatedActionsParams({ + rule, + ruleThrottle: legacyRuleActionsSO.saved_objects[0].attributes.ruleThrottle, + actions: legacyRuleActionsSO.saved_objects[0].attributes.actions, + references: legacyRuleActionsSO.saved_objects[0].references, + }); - const { id, ...restOfRule } = rule; - const migratedRule = { - ...restOfRule, - actions: siemNotification.data[0].actions, - throttle: siemNotification.data[0].schedule.interval, - notifyWhen: transformToNotifyWhen(siemNotification.data[0].throttle), - }; await rulesClient.update({ id: rule.id, data: migratedRule, }); + return { id: rule.id, ...migratedRule }; } - return rule; }; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts index 5a62513b1ab38..41394144e9c66 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts @@ -17,9 +17,7 @@ const baseAllowlistFields: AllowlistFields = { command_line: true, hash: true, pid: true, - pe: { - original_file_name: true, - }, + pe: true, uptime: true, Ext: { architecture: true, @@ -27,11 +25,15 @@ const baseAllowlistFields: AllowlistFields = { dll: true, malware_signature: true, memory_region: true, + protection: true, real: { entity_id: true, }, token: { + elevation: true, + elevation_type: true, integrity_level_name: true, + security_attributes: true, }, }, thread: true, @@ -44,10 +46,9 @@ const allowlistBaseEventFields: AllowlistFields = { name: true, path: true, code_signature: true, + hash: true, malware_signature: true, - pe: { - original_file_name: true, - }, + pe: true, }, dns: true, event: true, @@ -61,6 +62,7 @@ const allowlistBaseEventFields: AllowlistFields = { mtime: true, directory: true, hash: true, + pe: true, Ext: { bytes_compressed: true, bytes_compressed_present: true, 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 71e8b8e11c215..240634bccdca4 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 @@ -58,6 +58,9 @@ describe('TelemetryEventsSender', () => { path: 'X', test: 'me', another: 'nope', + pe: { + original_file_name: 'malware.exe', + }, Ext: { bytes_compressed: 'data up to 4mb', bytes_compressed_present: 'data up to 4mb', @@ -89,6 +92,9 @@ describe('TelemetryEventsSender', () => { executable: null, // null fields are never allowlisted working_directory: '/some/usr/dir', entity_id: 'some_entity_id', + Ext: { + protection: 'PsProtectedSignerAntimalware-Light', + }, }, Responses: '{ "result": 0 }', // >= 7.15 Target: { @@ -132,6 +138,9 @@ describe('TelemetryEventsSender', () => { size: 3, created: 0, path: 'X', + pe: { + original_file_name: 'malware.exe', + }, Ext: { bytes_compressed: 'data up to 4mb', bytes_compressed_present: 'data up to 4mb', @@ -159,6 +168,9 @@ describe('TelemetryEventsSender', () => { name: 'foo.exe', working_directory: '/some/usr/dir', entity_id: 'some_entity_id', + Ext: { + protection: 'PsProtectedSignerAntimalware-Light', + }, }, Responses: '{ "result": 0 }', Target: { diff --git a/x-pack/plugins/session_view/public/components/process_tree_alert/__snapshots__/index.test.tsx.snap b/x-pack/plugins/session_view/public/components/process_tree_alert/__snapshots__/index.test.tsx.snap index 9614bb1267ea5..b26778c8f8eea 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alert/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/session_view/public/components/process_tree_alert/__snapshots__/index.test.tsx.snap @@ -6,7 +6,7 @@ Object { "baseElement":
@@ -28,7 +28,7 @@ Object { data-euiicon-type="alert" />
cmd test alert
@@ -52,7 +52,7 @@ Object { , "container":
@@ -74,7 +74,7 @@ Object { data-euiicon-type="alert" />
cmd test alert
diff --git a/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx index 954492782ab93..f01f08f8e3095 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx @@ -73,7 +73,9 @@ export const ProcessTreeAlert = ({ onClick={handleExpandClick} /> - {dataOrDash(name)} + + {dataOrDash(name)} + {dataOrDash(status)} diff --git a/x-pack/plugins/session_view/public/components/process_tree_alert/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_alert/styles.ts index db06be06cc2aa..e8d3b0374c978 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alert/styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree_alert/styles.ts @@ -6,8 +6,9 @@ */ import { useMemo } from 'react'; -import { useEuiTheme, transparentize } from '@elastic/eui'; +import { transparentize } from '@elastic/eui'; import { CSSObject } from '@emotion/react'; +import { useEuiTheme } from '../../hooks'; interface StylesDeps { isInvestigated: boolean; @@ -15,7 +16,7 @@ interface StylesDeps { } export const useStyles = ({ isInvestigated, isSelected }: StylesDeps) => { - const { euiTheme } = useEuiTheme(); + const { euiTheme, euiVars } = useEuiTheme(); const cached = useMemo(() => { const { size, colors, font } = euiTheme; @@ -45,8 +46,7 @@ export const useStyles = ({ isInvestigated, isSelected }: StylesDeps) => { display: 'flex', gap: size.s, alignItems: 'center', - minHeight: '20px', - padding: `${size.xs} ${size.base}`, + padding: `0 ${size.base}`, boxSizing: 'content-box', cursor: 'pointer', '&:not(:last-child)': { @@ -56,9 +56,12 @@ export const useStyles = ({ isInvestigated, isSelected }: StylesDeps) => { '&:hover': { background: hoverBgColor, }, - button: { + '&& button': { flexShrink: 0, marginRight: size.s, + '&:hover, &:focus, &:focus-within': { + backgroundColor: transparentize(euiVars.buttonsBackgroundNormalDefaultPrimary, 0.2), + }, }, }; @@ -66,11 +69,17 @@ export const useStyles = ({ isInvestigated, isSelected }: StylesDeps) => { textTransform: 'capitalize', }; + const alertName: CSSObject = { + padding: `${size.xs} 0`, + color: colors.title, + }; + return { alert, alertStatus, + alertName, }; - }, [euiTheme, isInvestigated, isSelected]); + }, [euiTheme, isInvestigated, isSelected, euiVars]); return cached; }; diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts index bd95d87258178..a82a2253f4b2f 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/styles.ts @@ -16,7 +16,7 @@ export const useStyles = () => { const { size, colors, border } = euiTheme; const container: CSSObject = { - margin: `${size.xs} ${size.base} 0 ${size.xs}`, + margin: `${size.xs} ${size.base} ${size.base} ${size.xs}`, color: colors.text, padding: `${size.s} 0`, borderStyle: 'solid', diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx index 7daceaa366c43..1eba726607c10 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx @@ -57,7 +57,9 @@ describe('ProcessTreeNode component', () => { it('should have an alternate rendering for a session leader', async () => { renderResult = mockedContext.render(); - expect(renderResult.container.textContent).toEqual(' bash started by vagrant'); + expect(renderResult.container.textContent?.replace(/\s+/g, ' ')).toEqual( + ' bash started by vagrant' + ); }); // commented out until we get new UX for orphans treatment aka disjointed tree diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx index d868dee04cbf5..db81734c65937 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -33,6 +33,8 @@ import { AlertButton, ChildrenProcessesButton } from './buttons'; import { useButtonStyles } from './use_button_styles'; import { KIBANA_DATE_FORMAT } from '../../../common/constants'; import { useStyles } from './styles'; +import { SplitText } from './split_text'; +import { Nbsp } from './nbsp'; export interface ProcessDeps { process: Process; @@ -255,14 +257,21 @@ export function ProcessTreeNode({ onClick={onProcessClicked} > {isSessionLeader ? ( - <> - {' '} - {dataOrDash(name || args?.[0])}{' '} - {' '} - {dataOrDash(user?.name)} - + + + + {dataOrDash(name || args?.[0])} + + + + + + + + {dataOrDash(user?.name)} + ) : ( - + <> {showTimestamp && ( {timeStampsNormal} @@ -270,13 +279,15 @@ export function ProcessTreeNode({ )} - {' '} - - {dataOrDash(workingDirectory)}  - {dataOrDash(args?.[0])}{' '} - {args?.slice(1).join(' ')} + + + {dataOrDash(workingDirectory)} + + {dataOrDash(args?.[0])} + + {args?.slice(1).join(' ') || ''} - + )} {showUserEscalation && ( diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/nbsp.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/nbsp.tsx new file mode 100644 index 0000000000000..65274cd0b6ade --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree_node/nbsp.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { CSSObject } from '@emotion/react'; + +const css: CSSObject = { + width: '6px', + display: 'inline-block', +}; + +// Renders a non-breaking space with a specific width. +export const Nbsp = () => { + return  ; +}; diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/split_text.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/split_text.test.tsx new file mode 100644 index 0000000000000..eaa18b18d81cf --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree_node/split_text.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { SplitText } from './split_text'; + +describe('SplitText component', () => { + it('should split a text into one span for each character', async () => { + const text = 'hello world'; + + const renderResult = render({text}); + for (const char of text.replace(/\s+/g, '').split('')) { + expect(await renderResult.findAllByText(char)).toBeTruthy(); + } + expect(renderResult.container.textContent?.replace(/\s+/g, ' ')).toEqual(text); + }); + it('should provide an acessible label for screen readers', async () => { + const text = 'hello world'; + + const renderResult = render({text}); + expect(renderResult.queryByRole('document', { name: text })).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/split_text.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/split_text.tsx new file mode 100644 index 0000000000000..d31590c9ab967 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/process_tree_node/split_text.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 { CSSObject } from '@emotion/react'; + +type Props = { + children: string; + role?: string; +}; + +const css: CSSObject = { + '&&': { + display: 'inline', + fontSize: 0, + lineHeight: 0, + }, +}; + +// Split a text into multiple spans, each of which a single character. This is +// useful for creating inline "like" text but still having control over the blocks +// exclusive features, such height or line-height. +// It adds a `aria-label` attribute to a parent span, which is used by screen readers to +// read the text as a single block. +export const SplitText = ({ children, role = 'document', ...props }: Props) => ( + + {children.split('').map(function (char, index) { + return ( + + ); + })} + +); diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts index e328c28be8bad..b68df480064b3 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree_node/styles.ts @@ -30,14 +30,15 @@ export const useStyles = ({ const cached = useMemo(() => { const { colors, border, size, font } = euiTheme; + const ALERT_INDICATOR_WIDTH = '3px'; + const LINE_HEIGHT = '21px'; + const FONT_SIZE = '13px'; const TREE_INDENT = `calc(${size.l} + ${size.xxs})`; const PROCESS_TREE_LEFT_PADDING = size.s; const darkText: CSSObject = { color: colors.text, fontFamily: font.familyCode, - paddingLeft: size.xxs, - paddingRight: size.xs, }; const children: CSSObject = { @@ -84,15 +85,19 @@ export const useStyles = ({ const { bgColor, borderColor, hoverColor, searchResColor } = getHighlightColors(); + const fontSpacingReset: CSSObject = { + fontSize: 0, + lineHeight: 0, + }; + const processNode: CSSObject = { + ...fontSpacingReset, display: 'block', cursor: 'pointer', position: 'relative', - padding: `${size.xs} 0px`, marginBottom: isSessionLeader ? size.s : '0px', '&:hover:before': { backgroundColor: hoverColor, - transform: `translateY(-${size.xs})`, }, '&:before': { position: 'absolute', @@ -100,10 +105,29 @@ export const useStyles = ({ pointerEvents: 'none', content: `''`, marginLeft: `calc(-${depth} * ${TREE_INDENT} - ${PROCESS_TREE_LEFT_PADDING})`, - borderLeft: `${size.xs} solid ${borderColor}`, + borderLeft: `${ALERT_INDICATOR_WIDTH} solid ${borderColor}`, backgroundColor: bgColor, width: `calc(100% + ${depth} * ${TREE_INDENT} + ${PROCESS_TREE_LEFT_PADDING})`, - transform: `translateY(-${size.xs})`, + }, + }; + + const textSection: CSSObject = { + marginLeft: size.s, + span: { + fontSize: FONT_SIZE, + lineHeight: LINE_HEIGHT, + verticalAlign: 'middle', + display: 'inline-block', + }, + }; + + const sessionLeader: CSSObject = { + ...fontSpacingReset, + 'span, b': { + fontSize: FONT_SIZE, + lineHeight: LINE_HEIGHT, + display: 'inline-block', + verticalAlign: 'middle', }, }; @@ -119,16 +143,17 @@ export const useStyles = ({ verticalAlign: 'middle', color: euiVars.euiTextSubduedColor, wordBreak: 'break-all', - minHeight: `calc(${size.l} - ${size.xxs})`, - lineHeight: `calc(${size.l} - ${size.xxs})`, + padding: `${size.xs} 0px`, + button: { + marginLeft: '6px', + marginRight: size.xxs, + }, }; const workingDir: CSSObject = { color: colors.successText, fontFamily: font.familyCode, fontWeight: font.weight.regular, - paddingLeft: size.s, - paddingRight: size.xxs, }; const timeStamp: CSSObject = { @@ -139,6 +164,7 @@ export const useStyles = ({ paddingRight: size.base, paddingLeft: size.xxl, position: 'relative', + lineHeight: LINE_HEIGHT, }; const alertDetails: CSSObject = { @@ -157,6 +183,8 @@ export const useStyles = ({ timeStamp, alertDetails, icon, + textSection, + sessionLeader, }; }, [depth, euiTheme, hasAlerts, hasInvestigatedAlert, isSelected, euiVars, isSessionLeader]); diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts b/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts index 04d3eb793bfda..8cd4e2c5004b4 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts +++ b/x-pack/plugins/session_view/public/components/process_tree_node/use_button_styles.ts @@ -37,7 +37,8 @@ export const useButtonStyles = () => { }, }, '&&:hover, &&:focus': { - background: transparentize(euiVars.euiColorVis6, 0.04), + background: transparentize(euiVars.euiColorVis6, 0.12), + textDecoration: 'none', }, '&.isExpanded > span svg': { transform: `rotate(180deg)`, @@ -61,7 +62,8 @@ export const useButtonStyles = () => { background: transparentize(euiVars.euiColorDanger, 0.04), border: `${border.width.thin} solid ${transparentize(euiVars.euiColorDanger, 0.48)}`, '&&:hover, &&:focus': { - background: transparentize(euiVars.euiColorDanger, 0.04), + background: transparentize(euiVars.euiColorDanger, 0.12), + textDecoration: 'none', }, '&.isExpanded': { color: colors.ghost, diff --git a/x-pack/plugins/session_view/public/hooks/use_eui_theme.ts b/x-pack/plugins/session_view/public/hooks/use_eui_theme.ts index f7021f6146f09..02f7dd479d2ac 100644 --- a/x-pack/plugins/session_view/public/hooks/use_eui_theme.ts +++ b/x-pack/plugins/session_view/public/hooks/use_eui_theme.ts @@ -13,6 +13,7 @@ type EuiThemeProps = Parameters; type ExtraEuiVars = { // eslint-disable-next-line @typescript-eslint/naming-convention euiColorVis6_asText: string; + buttonsBackgroundNormalDefaultPrimary: string; }; type EuiVars = typeof euiLightVars & ExtraEuiVars; type EuiThemeReturn = ReturnType & { euiVars: EuiVars }; @@ -29,6 +30,7 @@ export const useEuiTheme = (...props: EuiThemeProps): EuiThemeReturn => { const extraEuiVars: ExtraEuiVars = { // eslint-disable-next-line @typescript-eslint/naming-convention euiColorVis6_asText: shade(themeVars.euiColorVis6, 0.335), + buttonsBackgroundNormalDefaultPrimary: '#006DE4', }; return { diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx index e753fee71d44b..6747c60bb840c 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { injectI18n } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { getDisplayValueFromFilter } from '@kbn/data-plugin/public'; @@ -30,7 +31,12 @@ export const ReadOnlyFilterItems = ({ filters, indexPatterns }: ReadOnlyFilterIt const filterList = filters.map((filter, index) => { const filterValue = getDisplayValueFromFilter(filter, indexPatterns); return ( - + { const path = Path.join(ES_ARCHIVE_DIR, folder); execSync( - `node ../../../../scripts/es_archiver load "${path}" --config ../../../test/functional/config.js`, + `node ../../../../scripts/es_archiver load "${path}" --config ../../../test/functional/config.base.js`, { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; @@ -24,14 +24,14 @@ export const esArchiverLoad = (folder: string) => { export const esArchiverUnload = (folder: string) => { const path = Path.join(ES_ARCHIVE_DIR, folder); execSync( - `node ../../../../scripts/es_archiver unload "${path}" --config ../../../test/functional/config.js`, + `node ../../../../scripts/es_archiver unload "${path}" --config ../../../test/functional/config.base.js`, { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; export const esArchiverResetKibana = () => { execSync( - `node ../../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.js`, + `node ../../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.base.js`, { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.test.tsx new file mode 100644 index 0000000000000..7040da99c39a3 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.test.tsx @@ -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 React from 'react'; +import { SyntheticsDatePicker } from './synthetics_date_picker'; +import { startPlugins } from '../../../utils/testing/__mocks__/synthetics_plugin_start_mock'; +import { createMemoryHistory } from 'history'; +import { render } from '../../../utils/testing'; +import { fireEvent } from '@testing-library/dom'; + +describe('SyntheticsDatePicker component', () => { + jest.setTimeout(10_000); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders properly with mock data', async () => { + const { findByText } = render(); + expect(await findByText('Last 15 minutes')).toBeInTheDocument(); + expect(await findByText('Refresh')).toBeInTheDocument(); + }); + + it('uses shared date range state when there is no url date range state', async () => { + const customHistory = createMemoryHistory({ + initialEntries: ['/?dateRangeStart=now-15m&dateRangeEnd=now'], + }); + + jest.spyOn(customHistory, 'push'); + + const { findByText } = render(, { + history: customHistory, + core: startPlugins, + }); + + expect(await findByText('~ 15 minutes ago')).toBeInTheDocument(); + + expect(await findByText('~ 30 minutes ago')).toBeInTheDocument(); + + expect(customHistory.push).toHaveBeenCalledWith({ + pathname: '/', + search: 'dateRangeEnd=now-15m&dateRangeStart=now-30m', + }); + }); + + it('should use url date range even if shared date range is present', async () => { + const customHistory = createMemoryHistory({ + initialEntries: ['/?g=%22%22&dateRangeStart=now-10m&dateRangeEnd=now'], + }); + + jest.spyOn(customHistory, 'push'); + + const { findByText } = render(, { + history: customHistory, + core: startPlugins, + }); + + expect(await findByText('Last 10 minutes')).toBeInTheDocument(); + + // it should update shared state + + expect(startPlugins.data.query.timefilter.timefilter.setTime).toHaveBeenCalledWith({ + from: 'now-10m', + to: 'now', + }); + }); + + it('should handle on change', async () => { + const customHistory = createMemoryHistory({ + initialEntries: ['/?g=%22%22&dateRangeStart=now-10m&dateRangeEnd=now'], + }); + + jest.spyOn(customHistory, 'push'); + + const { findByText, getByTestId, findByTestId } = render(, { + history: customHistory, + core: startPlugins, + }); + + expect(await findByText('Last 10 minutes')).toBeInTheDocument(); + + fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton')); + + fireEvent.click(await findByTestId('superDatePickerCommonlyUsed_Today')); + + expect(await findByText('Today')).toBeInTheDocument(); + + // it should update shared state + + expect(startPlugins.data.query.timefilter.timefilter.setTime).toHaveBeenCalledTimes(2); + + expect(startPlugins.data.query.timefilter.timefilter.setTime).toHaveBeenCalledWith({ + from: 'now-10m', + to: 'now', + }); + + expect(startPlugins.data.query.timefilter.timefilter.setTime).toHaveBeenLastCalledWith({ + from: 'now/d', + to: 'now', + }); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.tsx new file mode 100644 index 0000000000000..b9da1c098716d --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/date_picker/synthetics_date_picker.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 React, { useContext, useEffect } from 'react'; +import { EuiSuperDatePicker } from '@elastic/eui'; +import { useUrlParams } from '../../../hooks'; +import { CLIENT_DEFAULTS } from '../../../../../../common/constants'; +import { + SyntheticsSettingsContext, + SyntheticsStartupPluginsContext, + SyntheticsRefreshContext, +} from '../../../contexts'; + +const isSyntheticsDefaultDateRange = (dateRangeStart: string, dateRangeEnd: string) => { + const { DATE_RANGE_START, DATE_RANGE_END } = CLIENT_DEFAULTS; + + return dateRangeStart === DATE_RANGE_START && dateRangeEnd === DATE_RANGE_END; +}; + +export const SyntheticsDatePicker = () => { + const [getUrlParams, updateUrl] = useUrlParams(); + const { commonlyUsedRanges } = useContext(SyntheticsSettingsContext); + const { refreshApp } = useContext(SyntheticsRefreshContext); + + const { data } = useContext(SyntheticsStartupPluginsContext); + + // read time from state and update the url + const sharedTimeState = data?.query.timefilter.timefilter.getTime(); + + const { + autorefreshInterval, + autorefreshIsPaused, + dateRangeStart: start, + dateRangeEnd: end, + } = getUrlParams(); + + useEffect(() => { + const { from, to } = sharedTimeState ?? {}; + // if it's synthetics default range, and we have shared state from kibana, let's use that + if (isSyntheticsDefaultDateRange(start, end) && (from !== start || to !== end)) { + updateUrl({ dateRangeStart: from, dateRangeEnd: to }); + } else if (from !== start || to !== end) { + // if it's coming url. let's update shared state + data?.query.timefilter.timefilter.setTime({ from: start, to: end }); + } + + // only need at start, rest date picker on change fucn will take care off + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const euiCommonlyUsedRanges = commonlyUsedRanges + ? commonlyUsedRanges.map( + ({ from, to, display }: { from: string; to: string; display: string }) => { + return { + start: from, + end: to, + label: display, + }; + } + ) + : CLIENT_DEFAULTS.COMMONLY_USED_DATE_RANGES; + + return ( + { + if (data?.query?.timefilter?.timefilter) { + data?.query.timefilter.timefilter.setTime({ from: startN, to: endN }); + } + + updateUrl({ dateRangeStart: startN, dateRangeEnd: endN }); + refreshApp(); + }} + onRefresh={refreshApp} + onRefreshChange={({ isPaused, refreshInterval }) => { + updateUrl({ + autorefreshInterval: + refreshInterval === undefined ? autorefreshInterval : refreshInterval, + autorefreshIsPaused: isPaused, + }); + }} + /> + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu.tsx new file mode 100644 index 0000000000000..69821360aab8b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { HeaderMenuPortal } from '@kbn/observability-plugin/public'; +import { AppMountParameters } from '@kbn/core/public'; +import { ActionMenuContent } from './action_menu_content'; + +export const ActionMenu = ({ appMountParameters }: { appMountParameters: AppMountParameters }) => ( + + + +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.test.tsx new file mode 100644 index 0000000000000..0439eff39d88c --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.test.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 { render } from '../../../utils/testing/rtl_helpers'; +import { ActionMenuContent } from './action_menu_content'; + +describe('ActionMenuContent', () => { + it('renders settings link', () => { + const { getByRole, getByText } = render(); + + const settingsAnchor = getByRole('link', { name: 'Navigate to the Uptime settings page' }); + expect(settingsAnchor.getAttribute('href')).toBe('/settings'); + expect(getByText('Settings')); + }); + + it('renders exploratory view link', () => { + const { getByLabelText, getByText } = render(); + + const analyzeAnchor = getByLabelText( + 'Navigate to the "Explore Data" view to visualize Synthetics/User data' + ); + + expect(analyzeAnchor.getAttribute('href')).toContain('/app/observability/exploratory-view'); + expect(getByText('Explore data')); + }); + + it('renders Add Data link', () => { + const { getByLabelText, getByText } = render(); + + const addDataAnchor = getByLabelText('Navigate to a tutorial about adding Uptime data'); + + // this href value is mocked, so it doesn't correspond to the real link + // that Kibana core services will provide + expect(addDataAnchor.getAttribute('href')).toBe('/home#/tutorial/uptimeMonitors'); + expect(getByText('Add data')); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx new file mode 100644 index 0000000000000..aaf41dc46bcaf --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiHeaderLinks, EuiToolTip, EuiHeaderLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useHistory, useRouteMatch } from 'react-router-dom'; +import { createExploratoryViewUrl } from '@kbn/observability-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useSyntheticsSettingsContext } from '../../../contexts'; +import { useGetUrlParams } from '../../../hooks'; +import { MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../../../common/constants'; +import { stringifyUrlParams } from '../../../utils/url_params'; +import { InspectorHeaderLink } from './inspector_header_link'; + +const ADD_DATA_LABEL = i18n.translate('xpack.synthetics.addDataButtonLabel', { + defaultMessage: 'Add data', +}); + +const ANALYZE_DATA = i18n.translate('xpack.synthetics.analyzeDataButtonLabel', { + defaultMessage: 'Explore data', +}); + +const ANALYZE_MESSAGE = i18n.translate('xpack.synthetics.analyzeDataButtonLabel.message', { + defaultMessage: + 'Explore Data allows you to select and filter result data in any dimension and look for the cause or impact of performance problems.', +}); + +export function ActionMenuContent(): React.ReactElement { + const kibana = useKibana(); + const { basePath } = useSyntheticsSettingsContext(); + const params = useGetUrlParams(); + const { dateRangeStart, dateRangeEnd } = params; + const history = useHistory(); + + const selectedMonitor = { + monitor: { id: undefined, name: 'test' }, + }; /* useSelector(monitorStatusSelector) TODO: Implement state for monitor status */ + + const detailRouteMatch = useRouteMatch(MONITOR_ROUTE); + const monitorId = selectedMonitor?.monitor?.id; + + const syntheticExploratoryViewLink = createExploratoryViewUrl( + { + reportType: 'kpi-over-time', + allSeries: [ + { + dataType: 'synthetics', + seriesType: 'area', + selectedMetricField: 'monitor.duration.us', + time: { from: dateRangeStart, to: dateRangeEnd }, + breakdown: monitorId ? 'observer.geo.name' : 'monitor.type', + reportDefinitions: { + 'monitor.name': + selectedMonitor?.monitor?.name && detailRouteMatch?.isExact === true + ? [selectedMonitor?.monitor?.name] + : [], + 'url.full': ['ALL_VALUES'], + }, + name: monitorId ? `${monitorId}-response-duration` : 'All monitors response duration', + }, + ], + }, + basePath + ); + + return ( + + {/* TODO: See if it's needed for new Synthetics App */} + + + + + + {ANALYZE_MESSAGE}

}> + + {ANALYZE_DATA} + +
+ + + {ADD_DATA_LABEL} + + +
+ ); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/inspector_header_link.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/inspector_header_link.tsx new file mode 100644 index 0000000000000..b1166d342ff3e --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/inspector_header_link.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 { EuiHeaderLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { enableInspectEsQueries, useInspectorContext } from '@kbn/observability-plugin/public'; +import { ClientPluginsStart } from '../../../../../plugin'; +import { useSyntheticsSettingsContext } from '../../../contexts'; + +export function InspectorHeaderLink() { + const { + services: { inspector, uiSettings }, + } = useKibana(); + + const { isDev } = useSyntheticsSettingsContext(); + + const { inspectorAdapters } = useInspectorContext(); + + const isInspectorEnabled = uiSettings?.get(enableInspectEsQueries); + + const inspect = () => { + inspector.open(inspectorAdapters); + }; + + if (!isInspectorEnabled && !isDev) { + return null; + } + + return ( + + {i18n.translate('xpack.synthetics.inspectButtonText', { + defaultMessage: 'Inspect', + })} + + ); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/not_found.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/not_found.test.tsx new file mode 100644 index 0000000000000..0bade3c639a5e --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/not_found.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { NotFoundPage } from './not_found'; +import { render } from '../../../utils/testing'; + +describe('NotFoundPage', () => { + it('render component', async () => { + const { findByText } = render(); + + expect(await findByText('Page not found')).toBeInTheDocument(); + expect(await findByText('Back to home')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/not_found.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/not_found.tsx new file mode 100644 index 0000000000000..c94a7a7a06b6a --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/not_found.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiEmptyPrompt, + EuiPanel, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n-react'; + +export const NotFoundPage = () => { + const history = useHistory(); + return ( + + + + +

+ +

+ + } + body={ + + + + } + /> +
+
+
+ ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.test.tsx new file mode 100644 index 0000000000000..bd58aa12b1c82 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.test.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. + */ + +// app.test.js +import React from 'react'; +import 'jest-styled-components'; +import { render } from '../../../utils/testing/rtl_helpers'; +import { SyntheticsPageTemplateComponent } from './synthetics_page_template'; +import { OVERVIEW_ROUTE } from '../../../../../../common/constants'; +import { useBreakpoints } from '../../../../../hooks/use_breakpoints'; + +jest.mock('../../../../../hooks/use_breakpoints', () => { + const down = jest.fn().mockReturnValue(false); + return { + useBreakpoints: () => ({ down }), + }; +}); + +describe('SyntheticsPageTemplateComponent', () => { + describe('styling', () => { + // In this test we use snapshots because we're asserting on generated + // styles. Writing assertions manually here could make this test really + // convoluted, and it require us to manually update styling strings + // according to `styled-components` generator, which is counter-productive. + // In general, however, we avoid snaphshot tests. + + it('does not apply header centering on bigger resolutions', () => { + const { container } = render(); + expect(container.firstChild).toBeDefined(); + }); + + it('applies the header centering on mobile', () => { + (useBreakpoints().down as jest.Mock).mockReturnValue(true); + const { container } = render(); + expect(container.firstChild).toBeDefined(); + }); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx new file mode 100644 index 0000000000000..44b38236fc2a2 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useMemo } from 'react'; +import styled from 'styled-components'; +import { EuiPageHeaderProps, EuiPageTemplateProps } from '@elastic/eui'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useInspectorContext } from '@kbn/observability-plugin/public'; +import { ClientPluginsStart } from '../../../../../plugin'; +import { useNoDataConfig } from '../../../hooks/use_no_data_config'; +import { EmptyStateLoading } from '../../overview/empty_state/empty_state_loading'; +import { EmptyStateError } from '../../overview/empty_state/empty_state_error'; +import { useHasData } from '../../overview/empty_state/use_has_data'; +import { useBreakpoints } from '../../../hooks'; + +interface Props { + path: string; + pageHeader?: EuiPageHeaderProps; +} + +const mobileCenteredHeader = ` + .euiPageHeaderContent > .euiFlexGroup > .euiFlexItem { + align-items: center; + } +`; + +export const SyntheticsPageTemplateComponent: React.FC = ({ + path, + pageHeader, + children, + ...pageTemplateProps +}) => { + const { + services: { observability }, + } = useKibana(); + const { down } = useBreakpoints(); + const isMobile = down('s'); + + const PageTemplateComponent = observability.navigation.PageTemplate; + const StyledPageTemplateComponent = useMemo(() => { + return styled(PageTemplateComponent)<{ isMobile: boolean }>` + .euiPageHeaderContent > .euiFlexGroup { + flex-wrap: wrap; + } + + ${(props) => (props.isMobile ? mobileCenteredHeader : '')} + `; + }, [PageTemplateComponent]); + + const noDataConfig = useNoDataConfig(); + + const { loading, error, data } = useHasData(); + const { inspectorAdapters } = useInspectorContext(); + + useEffect(() => { + inspectorAdapters.requests.reset(); + }, [inspectorAdapters.requests]); + + if (error) { + return ; + } + + const showLoading = loading && !data; + + return ( + <> + + {showLoading && } +
+ {children} +
+
+ + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/wrappers/service_allowed_wrapper.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/wrappers/service_allowed_wrapper.tsx new file mode 100644 index 0000000000000..163ed0d37b7c3 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/wrappers/service_allowed_wrapper.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 from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; +import { useSyntheticsServiceAllowed } from '../../../hooks/use_service_allowed'; + +export const ServiceAllowedWrapper: React.FC = ({ children }) => { + const { isAllowed, signupUrl, loading } = useSyntheticsServiceAllowed(); + + if (loading) { + return ( + } + title={

{LOADING_MONITOR_MANAGEMENT_LABEL}

} + /> + ); + } + + // checking for explicit false + if (isAllowed === false) { + return ( + {MONITOR_MANAGEMENT_LABEL}} + body={

{PUBLIC_BETA_DESCRIPTION}

} + actions={[ + + {REQUEST_ACCESS_LABEL} + , + ]} + /> + ); + } + + return <>{children}; +}; + +const REQUEST_ACCESS_LABEL = i18n.translate('xpack.synthetics.monitorManagement.requestAccess', { + defaultMessage: 'Request access', +}); + +export const MONITOR_MANAGEMENT_LABEL = i18n.translate('xpack.synthetics.monitorManagement.label', { + defaultMessage: 'Monitor Management', +}); + +const LOADING_MONITOR_MANAGEMENT_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.loading.label', + { + defaultMessage: 'Loading Monitor Management', + } +); + +export const PUBLIC_BETA_DESCRIPTION = i18n.translate( + 'xpack.synthetics.monitorManagement.publicBetaDescription', + { + defaultMessage: + "We've got a brand new app on the way. In the meantime, we're excited to give you early access to our globally managed testing infrastructure. This will allow you to upload synthetic monitors using our new point and click script recorder and manage your monitors with a new UI.", + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_edit_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_edit_page.tsx new file mode 100644 index 0000000000000..d0a0c04150f8d --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/monitor_add_edit_page.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useTrackPageview } from '@kbn/observability-plugin/public'; +import { useMonitorAddEditBreadcrumbs } from './use_breadcrumbs'; + +export const MonitorAddEditPage: React.FC = () => { + useTrackPageview({ app: 'synthetics', path: 'add-monitor' }); + useTrackPageview({ app: 'synthetics', path: 'add-monitor', delay: 15000 }); + useMonitorAddEditBreadcrumbs(); + + return ( + <> +

Monitor Add or Edit page

+ + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/use_breadcrumbs.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/use_breadcrumbs.ts new file mode 100644 index 0000000000000..2fb17340c25fc --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/use_breadcrumbs.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 { i18n } from '@kbn/i18n'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; +import { MONITOR_ADD_ROUTE } from '../../../../../common/constants'; +import { PLUGIN } from '../../../../../common/constants/plugin'; + +export const useMonitorAddEditBreadcrumbs = () => { + const kibana = useKibana(); + const appPath = kibana.services.application?.getUrlForApp(PLUGIN.SYNTHETICS_PLUGIN_ID) ?? ''; + + useBreadcrumbs([ + { + text: ADD_MONITOR_CRUMB, + href: `${appPath}/${MONITOR_ADD_ROUTE}`, + }, + ]); +}; + +export const ADD_MONITOR_CRUMB = i18n.translate( + 'xpack.synthetics.monitorManagement.addMonitorCrumb', + { + defaultMessage: 'Add monitor', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/monitor_management_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/monitor_management_page.tsx new file mode 100644 index 0000000000000..c4ca665563f0d --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/monitor_management_page.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useTrackPageview } from '@kbn/observability-plugin/public'; +import { useMonitorManagementBreadcrumbs } from './use_breadcrumbs'; + +export const MonitorManagementPage: React.FC = () => { + useTrackPageview({ app: 'synthetics', path: 'manage-monitors' }); + useTrackPageview({ app: 'synthetics', path: 'manage-monitors', delay: 15000 }); + useMonitorManagementBreadcrumbs(); + + return ( + <> +

Monitor Management List page (Monitor Management Page)

+ + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/use_breadcrumbs.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/use_breadcrumbs.ts new file mode 100644 index 0000000000000..be94df18ef7d2 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/use_breadcrumbs.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 { i18n } from '@kbn/i18n'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; +import { MONITOR_MANAGEMENT_ROUTE } from '../../../../../common/constants'; +import { PLUGIN } from '../../../../../common/constants/plugin'; + +export const useMonitorManagementBreadcrumbs = () => { + const kibana = useKibana(); + const appPath = kibana.services.application?.getUrlForApp(PLUGIN.SYNTHETICS_PLUGIN_ID) ?? ''; + + useBreadcrumbs([ + { + text: MONITOR_MANAGEMENT_CRUMB, + href: `${appPath}/${MONITOR_MANAGEMENT_ROUTE}`, + }, + ]); +}; + +const MONITOR_MANAGEMENT_CRUMB = i18n.translate( + 'xpack.synthetics.monitorManagementPage.monitorManagementCrumb', + { + defaultMessage: 'Monitor Management', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/empty_state_error.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/empty_state_error.tsx new file mode 100644 index 0000000000000..3f2150169e2df --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/empty_state_error.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiEmptyPrompt, EuiPanel, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { Fragment } from 'react'; +import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; + +interface EmptyStateErrorProps { + errors: Array>; +} + +export const EmptyStateError = ({ errors }: EmptyStateErrorProps) => { + const unauthorized = errors.find( + (error) => error.message && error.message.includes('unauthorized') + ); + + return ( + + + + + {unauthorized ? ( +

+ {i18n.translate('xpack.synthetics.emptyStateError.notAuthorized', { + defaultMessage: + 'You are not authorized to view Uptime data, please contact your system administrator.', + })} +

+ ) : ( +

+ {i18n.translate('xpack.synthetics.emptyStateError.title', { + defaultMessage: 'Error', + })} +

+ )} + + } + body={ + + {!unauthorized && + errors.map((error) => ( +

+ {error.body?.message || error.message} +

+ ))} +
+ } + /> +
+
+
+ ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/empty_state_loading.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/empty_state_loading.tsx new file mode 100644 index 0000000000000..0f71c9bafa962 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/empty_state_loading.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 { EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { Fragment } from 'react'; + +export const EmptyStateLoading = () => ( + + + + +

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

+
+ + } + /> +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/use_has_data.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/use_has_data.tsx new file mode 100644 index 0000000000000..3561d841c7564 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/use_has_data.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useContext, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { getIndexStatus, selectIndexState } from '../../../state'; +import { SyntheticsRefreshContext } from '../../../contexts'; +// import { getDynamicSettings } from '../../../state/actions/dynamic_settings'; + +export const useHasData = () => { + const { loading, error, data } = useSelector(selectIndexState); + const { lastRefresh } = useContext(SyntheticsRefreshContext); + + // const { settings } = useSelector(selectDynamicSettings); // TODO: Add state for dynamicSettings + const settings = { heartbeatIndices: 'synthetics-*' }; + + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(getIndexStatus()); + }, [dispatch, lastRefresh]); + + /* useEffect(() => { + dispatch(getDynamicSettings()); + }, [dispatch]);*/ + + return { + data, + error, + loading, + settings, + }; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/overview_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/overview_page.tsx new file mode 100644 index 0000000000000..4a370fc024423 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/overview_page.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import React from 'react'; +import { useTrackPageview } from '@kbn/observability-plugin/public'; +import { useSyntheticsSettingsContext } from '../../contexts'; +import { useOverviewBreadcrumbs } from './use_breadcrumbs'; + +export const OverviewPage: React.FC = () => { + useTrackPageview({ app: 'synthetics', path: 'overview' }); + useTrackPageview({ app: 'synthetics', path: 'overview', delay: 15000 }); + useOverviewBreadcrumbs(); + const { basePath } = useSyntheticsSettingsContext(); + + return ( + + +

This page should show empty state or overview

+
+ + Monitor Management + + + Add Monitor + +
+ ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/use_breadcrumbs.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/use_breadcrumbs.ts new file mode 100644 index 0000000000000..d33a0fd3c20cc --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/use_breadcrumbs.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 { i18n } from '@kbn/i18n'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; +import { PLUGIN } from '../../../../../common/constants/plugin'; + +export const useOverviewBreadcrumbs = () => { + const kibana = useKibana(); + const appPath = kibana.services.application?.getUrlForApp(PLUGIN.SYNTHETICS_PLUGIN_ID) ?? ''; + + useBreadcrumbs([ + { + text: OVERVIEW_PAGE_CRUMB, + href: `${appPath}`, + }, + ]); +}; + +export const OVERVIEW_PAGE_CRUMB = i18n.translate('xpack.synthetics.overviewPage.overviewCrumb', { + defaultMessage: 'Overview', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/contexts/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/index.ts new file mode 100644 index 0000000000000..6a2e0226671fe --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './synthetics_refresh_context'; +export * from './synthetics_settings_context'; +export * from './synthetics_theme_context'; +export * from './synthetics_startup_plugins_context'; +export * from './synthetics_data_view_context'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_data_view_context.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_data_view_context.tsx new file mode 100644 index 0000000000000..af657abd77540 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_data_view_context.tsx @@ -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 React, { createContext, useContext } from 'react'; +import { useFetcher } from '@kbn/observability-plugin/public'; +import { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public'; +import { useHasData } from '../components/overview/empty_state/use_has_data'; + +export const SyntheticsDataViewContext = createContext({} as DataView); + +export const SyntheticsDataViewContextProvider: React.FC<{ + dataViews: DataViewsPublicPluginStart; +}> = ({ children, dataViews }) => { + const { settings, data: indexStatus } = useHasData(); + + const heartbeatIndices = settings?.heartbeatIndices || ''; + + const { data } = useFetcher>(async () => { + if (heartbeatIndices && indexStatus?.indexExists) { + // this only creates an dateView in memory, not as saved object + return dataViews.create({ title: heartbeatIndices }); + } + }, [heartbeatIndices, indexStatus?.indexExists]); + + return ; +}; + +export const useSyntheticsDataView = () => useContext(SyntheticsDataViewContext); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_refresh_context.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_refresh_context.tsx new file mode 100644 index 0000000000000..696054eeb618b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_refresh_context.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, { createContext, useContext, useMemo, useState } from 'react'; + +interface SyntheticsRefreshContext { + lastRefresh: number; + refreshApp: () => void; +} + +const defaultContext: SyntheticsRefreshContext = { + lastRefresh: 0, + refreshApp: () => { + throw new Error('App refresh was not initialized, set it when you invoke the context'); + }, +}; + +export const SyntheticsRefreshContext = createContext(defaultContext); + +export const SyntheticsRefreshContextProvider: React.FC = ({ children }) => { + const [lastRefresh, setLastRefresh] = useState(Date.now()); + + const refreshApp = () => { + const refreshTime = Date.now(); + setLastRefresh(refreshTime); + }; + + const value = useMemo(() => { + return { lastRefresh, refreshApp }; + }, [lastRefresh]); + + return ; +}; + +export const useSyntheticsRefreshContext = () => useContext(SyntheticsRefreshContext); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_settings_context.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_settings_context.tsx new file mode 100644 index 0000000000000..61231cfde6dfe --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_settings_context.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + AppMountParameters, + ChromeBadge, + ChromeBreadcrumb, + CoreStart, + I18nStart, +} from '@kbn/core/public'; +import React, { createContext, useContext, useMemo } from 'react'; +import { ClientPluginsSetup, ClientPluginsStart } from '../../../plugin'; +import { CLIENT_DEFAULTS, CONTEXT_DEFAULTS } from '../../../../common/constants'; +import { useGetUrlParams } from '../hooks'; + +export interface CommonlyUsedDateRange { + from: string; + to: string; + display: string; +} + +export interface SyntheticsAppProps { + basePath: string; + canSave: boolean; + core: CoreStart; + darkMode: boolean; + i18n: I18nStart; + isApmAvailable: boolean; + isInfraAvailable: boolean; + isLogsAvailable: boolean; + plugins: ClientPluginsSetup; + startPlugins: ClientPluginsStart; + setBadge: (badge?: ChromeBadge) => void; + renderGlobalHelpControls(): void; + commonlyUsedRanges: CommonlyUsedDateRange[]; + setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; + appMountParameters: AppMountParameters; + isDev: boolean; +} + +export interface SyntheticsSettingsContextValues { + basePath: string; + dateRangeStart: string; + dateRangeEnd: string; + isApmAvailable: boolean; + isInfraAvailable: boolean; + isLogsAvailable: boolean; + commonlyUsedRanges?: CommonlyUsedDateRange[]; + isDev?: boolean; +} + +const { BASE_PATH } = CONTEXT_DEFAULTS; + +const { DATE_RANGE_START, DATE_RANGE_END } = CLIENT_DEFAULTS; + +/** + * These are default values for the context. These defaults are typically + * overwritten by the Synthetics App upon its invocation. + */ +const defaultContext: SyntheticsSettingsContextValues = { + basePath: BASE_PATH, + dateRangeStart: DATE_RANGE_START, + dateRangeEnd: DATE_RANGE_END, + isApmAvailable: true, + isInfraAvailable: true, + isLogsAvailable: true, + isDev: false, +}; +export const SyntheticsSettingsContext = createContext(defaultContext); + +export const SyntheticsSettingsContextProvider: React.FC = ({ + children, + ...props +}) => { + const { basePath, isApmAvailable, isInfraAvailable, isLogsAvailable, commonlyUsedRanges, isDev } = + props; + + const { dateRangeStart, dateRangeEnd } = useGetUrlParams(); + + const value = useMemo(() => { + return { + isDev, + basePath, + isApmAvailable, + isInfraAvailable, + isLogsAvailable, + commonlyUsedRanges, + dateRangeStart: dateRangeStart ?? DATE_RANGE_START, + dateRangeEnd: dateRangeEnd ?? DATE_RANGE_END, + }; + }, [ + isDev, + basePath, + isApmAvailable, + isInfraAvailable, + isLogsAvailable, + dateRangeStart, + dateRangeEnd, + commonlyUsedRanges, + ]); + + return ; +}; + +export const useSyntheticsSettingsContext = () => useContext(SyntheticsSettingsContext); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_startup_plugins_context.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_startup_plugins_context.tsx new file mode 100644 index 0000000000000..75ee7b3d6ae67 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_startup_plugins_context.tsx @@ -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 React, { createContext, useContext } from 'react'; +import { ClientPluginsStart } from '../../../plugin'; + +export const SyntheticsStartupPluginsContext = createContext>({}); + +export const SyntheticsStartupPluginsContextProvider: React.FC> = ({ + children, + ...props +}) => ; + +export const useSyntheticsStartPlugins = () => useContext(SyntheticsStartupPluginsContext); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_theme_context.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_theme_context.tsx new file mode 100644 index 0000000000000..32844de9d04c2 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_theme_context.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 { euiLightVars, euiDarkVars } from '@kbn/ui-theme'; +import React, { createContext, useMemo } from 'react'; +import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; +import { DARK_THEME, LIGHT_THEME, PartialTheme, Theme } from '@elastic/charts'; + +export interface SyntheticsAppColors { + danger: string; + dangerBehindText: string; + success: string; + gray: string; + range: string; + mean: string; + warning: string; + lightestShade: string; +} + +export interface SyntheticsThemeContextValues { + colors: SyntheticsAppColors; + chartTheme: { + baseTheme?: Theme; + theme?: PartialTheme; + }; +} + +/** + * These are default values for the context. These defaults are typically + * overwritten by the Synthetics App upon its invocation. + */ +const defaultContext: SyntheticsThemeContextValues = { + colors: { + danger: euiLightVars.euiColorDanger, + dangerBehindText: euiDarkVars.euiColorVis9_behindText, + mean: euiLightVars.euiColorPrimary, + range: euiLightVars.euiFocusBackgroundColor, + success: euiLightVars.euiColorSuccess, + warning: euiLightVars.euiColorWarning, + gray: euiLightVars.euiColorLightShade, + lightestShade: euiLightVars.euiColorLightestShade, + }, + chartTheme: { + baseTheme: LIGHT_THEME, + theme: EUI_CHARTS_THEME_LIGHT.theme, + }, +}; + +export const SyntheticsThemeContext = createContext(defaultContext); + +interface ThemeContextProps { + darkMode: boolean; +} + +export const SyntheticsThemeContextProvider: React.FC = ({ + darkMode, + children, +}) => { + let colors: SyntheticsAppColors; + if (darkMode) { + colors = { + danger: euiDarkVars.euiColorVis9, + dangerBehindText: euiDarkVars.euiColorVis9_behindText, + mean: euiDarkVars.euiColorPrimary, + gray: euiDarkVars.euiColorLightShade, + range: euiDarkVars.euiFocusBackgroundColor, + success: euiDarkVars.euiColorSuccess, + warning: euiDarkVars.euiColorWarning, + lightestShade: euiDarkVars.euiColorLightestShade, + }; + } else { + colors = { + danger: euiLightVars.euiColorVis9, + dangerBehindText: euiLightVars.euiColorVis9_behindText, + mean: euiLightVars.euiColorPrimary, + gray: euiLightVars.euiColorLightShade, + range: euiLightVars.euiFocusBackgroundColor, + success: euiLightVars.euiColorSuccess, + warning: euiLightVars.euiColorWarning, + lightestShade: euiLightVars.euiColorLightestShade, + }; + } + const value = useMemo(() => { + return { + colors, + chartTheme: { + baseTheme: darkMode ? DARK_THEME : LIGHT_THEME, + theme: darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme, + }, + }; + }, [colors, darkMode]); + + return ; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts new file mode 100644 index 0000000000000..15079dc68823b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './use_url_params'; +export * from './use_breadcrumbs'; +export * from './use_telemetry'; +export * from '../../../hooks/use_breakpoints'; +export * from './use_service_allowed'; +export * from './use_no_data_config'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx new file mode 100644 index 0000000000000..d2639da47e79e --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ChromeBreadcrumb } from '@kbn/core/public'; +import { render } from '../utils/testing'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import { OVERVIEW_ROUTE } from '../../../../common/constants'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { + SyntheticsUrlParams, + getSupportedUrlParams, +} from '../utils/url_params/get_supported_url_params'; +import { makeBaseBreadcrumb, useBreadcrumbs } from './use_breadcrumbs'; + +describe('useBreadcrumbs', () => { + it('sets the given breadcrumbs', () => { + const [getBreadcrumbs, core] = mockCore(); + + const expectedCrumbs: ChromeBreadcrumb[] = [ + { text: 'Crumb: ', href: 'http://href.example.net' }, + { text: 'Crumb II: Son of Crumb', href: 'http://href2.example.net' }, + ]; + + const Component = () => { + useBreadcrumbs(expectedCrumbs); + return <>Hello; + }; + + render( + + + + + + ); + + const urlParams: SyntheticsUrlParams = getSupportedUrlParams({}); + expect(JSON.stringify(getBreadcrumbs())).toEqual( + JSON.stringify( + makeBaseBreadcrumb('/app/synthetics', '/app/observability', urlParams).concat( + expectedCrumbs + ) + ) + ); + }); +}); + +const mockCore: () => [() => ChromeBreadcrumb[], any] = () => { + let breadcrumbObj: ChromeBreadcrumb[] = []; + const get = () => { + return breadcrumbObj; + }; + const core = { + application: { + getUrlForApp: (app: string) => + app === 'synthetics' ? '/app/synthetics' : '/app/observability', + navigateToUrl: jest.fn(), + }, + chrome: { + setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => { + breadcrumbObj = newBreadcrumbs; + }, + }, + }; + + return [get, core]; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.ts new file mode 100644 index 0000000000000..0902481c51a9c --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.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 { ChromeBreadcrumb } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import { MouseEvent, useEffect } from 'react'; +import { EuiBreadcrumb } from '@elastic/eui'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { SyntheticsUrlParams, stringifyUrlParams } from '../utils/url_params'; +import { useUrlParams } from './use_url_params'; +import { PLUGIN } from '../../../../common/constants/plugin'; + +const EMPTY_QUERY = '?'; + +function handleBreadcrumbClick( + breadcrumbs: ChromeBreadcrumb[], + navigateToHref?: (url: string) => Promise +) { + return breadcrumbs.map((bc) => ({ + ...bc, + ...(bc.href + ? { + onClick: (event: MouseEvent) => { + if (navigateToHref && bc.href) { + event.preventDefault(); + navigateToHref(bc.href); + } + }, + } + : {}), + })); +} + +export const makeBaseBreadcrumb = ( + uptimePath: string, + observabilityPath: string, + params?: SyntheticsUrlParams +): [EuiBreadcrumb, EuiBreadcrumb] => { + if (params) { + const crumbParams: Partial = { ...params }; + + delete crumbParams.statusFilter; + const query = stringifyUrlParams(crumbParams, true); + uptimePath += query === EMPTY_QUERY ? '' : query; + } + + return [ + { + text: i18n.translate('xpack.synthetics.breadcrumbs.observabilityText', { + defaultMessage: 'Observability', + }), + href: observabilityPath, + }, + { + text: i18n.translate('xpack.synthetics.breadcrumbs.overviewBreadcrumbText', { + defaultMessage: 'Synthetics', + }), + href: uptimePath, + }, + ]; +}; + +export const useBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => { + const params = useUrlParams()[0](); + const kibana = useKibana(); + const setBreadcrumbs = kibana.services.chrome?.setBreadcrumbs; + const uptimePath = kibana.services.application?.getUrlForApp(PLUGIN.SYNTHETICS_PLUGIN_ID) ?? ''; + const observabilityPath = + kibana.services.application?.getUrlForApp('observability-overview') ?? ''; + const navigate = kibana.services.application?.navigateToUrl; + + useEffect(() => { + if (setBreadcrumbs) { + setBreadcrumbs( + handleBreadcrumbClick( + makeBaseBreadcrumb(uptimePath, observabilityPath, params).concat(extraCrumbs), + navigate + ) + ); + } + }, [uptimePath, observabilityPath, extraCrumbs, navigate, params, setBreadcrumbs]); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_no_data_config.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_no_data_config.ts new file mode 100644 index 0000000000000..6243d2f5d8c8a --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_no_data_config.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useContext } from 'react'; +import { useSelector } from 'react-redux'; +import { KibanaPageTemplateProps, useKibana } from '@kbn/kibana-react-plugin/public'; +import { SyntheticsSettingsContext } from '../contexts'; +import { ClientPluginsStart } from '../../../plugin'; +import { selectIndexState } from '../state'; + +export function useNoDataConfig(): KibanaPageTemplateProps['noDataConfig'] { + const { basePath } = useContext(SyntheticsSettingsContext); + + const { + services: { docLinks }, + } = useKibana(); + + const { data } = useSelector(selectIndexState); + + // Returns no data config when there is no historical data + if (data && !data.indexExists) { + return { + solution: i18n.translate('xpack.synthetics.noDataConfig.solutionName', { + defaultMessage: 'Observability', + }), + actions: { + beats: { + title: i18n.translate('xpack.synthetics.noDataConfig.beatsCard.title', { + defaultMessage: 'Add monitors with Heartbeat', + }), + description: i18n.translate('xpack.synthetics.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, + }; + } +} +// TODO: Change no data config for Synthetics diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_service_allowed.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_service_allowed.ts new file mode 100644 index 0000000000000..12a2ed7043973 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_service_allowed.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 { useDispatch } from 'react-redux'; +import { useEffect } from 'react'; +// import { syntheticsServiceAllowedSelector } from '../../../state/selectors'; +// import { getSyntheticsServiceAllowed } from '../../../state/actions'; + +export const useSyntheticsServiceAllowed = () => { + const dispatch = useDispatch(); + + useEffect(() => { + // dispatch(getSyntheticsServiceAllowed.get()); + }, [dispatch]); + + // return useSelector(syntheticsServiceAllowedSelector); + // TODO Implement for Synthetics App + return { isAllowed: true, signupUrl: 'https://example.com', loading: false }; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.ts new file mode 100644 index 0000000000000..64ecabaff5d5a --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.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 { useEffect } from 'react'; +import { useGetUrlParams } from './use_url_params'; +import { apiService } from '../../../utils/api_service'; +// import { API_URLS } from '../../../common/constants'; + +export enum SyntheticsPage { + Overview = 'Overview', + Monitor = 'Monitor', + MonitorAdd = 'AddMonitor', + MonitorEdit = 'EditMonitor', + MonitorManagement = 'MonitorManagement', + Settings = 'Settings', + Certificates = 'Certificates', + StepDetail = 'StepDetail', + SyntheticCheckStepsPage = 'SyntheticCheckStepsPage', + NotFound = '__not-found__', +} + +export const useSyntheticsTelemetry = (page?: SyntheticsPage) => { + const { dateRangeStart, dateRangeEnd, autorefreshInterval, autorefreshIsPaused } = + useGetUrlParams(); + + useEffect(() => { + if (!apiService.http) throw new Error('Core http services are not defined'); + + /* + TODO: Add/Modify telemetry endpoint for synthetics + const params = { + page, + autorefreshInterval: autorefreshInterval / 1000, // divide by 1000 to keep it in secs + dateStart: dateRangeStart, + dateEnd: dateRangeEnd, + autoRefreshEnabled: !autorefreshIsPaused, + };*/ + setTimeout(() => { + // apiService.post(API_URLS.LOG_PAGE_VIEW, params); + }, 100); + }, [autorefreshInterval, autorefreshIsPaused, dateRangeEnd, dateRangeStart, page]); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_url_params.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_url_params.test.tsx new file mode 100644 index 0000000000000..d1e9ceae998b5 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_url_params.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import DateMath from '@kbn/datemath'; +import userEvent from '@testing-library/user-event'; +import { render } from '../utils/testing'; +import React, { useState, Fragment } from 'react'; +import { useUrlParams, SyntheticsUrlParamsHook } from './use_url_params'; +import { SyntheticsRefreshContext } from '../contexts'; + +interface MockUrlParamsComponentProps { + hook: SyntheticsUrlParamsHook; + updateParams?: { [key: string]: any }; +} + +const UseUrlParamsTestComponent = ({ hook, updateParams }: MockUrlParamsComponentProps) => { + const [params, setParams] = useState({}); + const [getUrlParams, updateUrlParams] = hook(); + const queryParams = getUrlParams(); + return ( + + {Object.keys(params).length > 0 ?
{JSON.stringify(params)}
: null} + + +
+ ); +}; + +describe('useUrlParams', () => { + let dateMathSpy: any; + const MOCK_DATE_VALUE = 20; + + beforeEach(() => { + dateMathSpy = jest.spyOn(DateMath, 'parse'); + dateMathSpy.mockReturnValue(MOCK_DATE_VALUE); + }); + + it('accepts router props, updates URL params, and returns the current params', async () => { + const { findByText, history } = render( + + + + ); + + const pushSpy = jest.spyOn(history, 'push'); + + const setUrlParamsButton = await findByText('Set url params'); + userEvent.click(setUrlParamsButton); + expect(pushSpy).toHaveBeenCalledWith({ + pathname: '/', + search: 'dateRangeEnd=now&dateRangeStart=now-12d', + }); + pushSpy.mockClear(); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_url_params.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_url_params.ts new file mode 100644 index 0000000000000..ec31c711cffc7 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_url_params.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect } from 'react'; +import { stringify } from 'query-string'; +import { useLocation, useHistory } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import { SyntheticsUrlParams, getSupportedUrlParams } from '../utils/url_params'; + +// TODO: Create the following imports for new Synthetics App +import { selectedFiltersSelector } from '../../../legacy_uptime/state/selectors'; +import { setSelectedFilters } from '../../../legacy_uptime/state/actions/selected_filters'; +import { getFiltersFromMap } from '../../../legacy_uptime/hooks/use_selected_filters'; +import { getParsedParams } from '../../../legacy_uptime/lib/helper/parse_search'; + +export type GetUrlParams = () => SyntheticsUrlParams; +export type UpdateUrlParams = (updatedParams: { + [key: string]: string | number | boolean | undefined; +}) => void; + +export type SyntheticsUrlParamsHook = () => [GetUrlParams, UpdateUrlParams]; + +export const useGetUrlParams: GetUrlParams = () => { + const { search } = useLocation(); + + return getSupportedUrlParams(getParsedParams(search)); +}; + +const getMapFromFilters = (value: any): Map | undefined => { + try { + return new Map(JSON.parse(value)); + } catch { + return undefined; + } +}; + +export const useUrlParams: SyntheticsUrlParamsHook = () => { + const { pathname, search } = useLocation(); + const history = useHistory(); + const dispatch = useDispatch(); + const selectedFilters = useSelector(selectedFiltersSelector); + const { filters } = useGetUrlParams(); + + useEffect(() => { + if (selectedFilters === null) { + const filterMap = getMapFromFilters(filters); + if (filterMap) { + dispatch(setSelectedFilters(getFiltersFromMap(filterMap))); + } + } + }, [dispatch, filters, selectedFilters]); + + const updateUrlParams: UpdateUrlParams = useCallback( + (updatedParams) => { + const currentParams = getParsedParams(search); + const mergedParams = { + ...currentParams, + ...updatedParams, + }; + + const updatedSearch = stringify( + // drop any parameters that have no value + Object.keys(mergedParams).reduce((params, key) => { + const value = mergedParams[key]; + if (value === undefined || value === '') { + return params; + } + return { + ...params, + [key]: value, + }; + }, {}) + ); + + // only update the URL if the search has actually changed + if (search !== updatedSearch) { + history.push({ pathname, search: updatedSearch }); + } + const filterMap = getMapFromFilters(mergedParams.filters); + if (!filterMap) { + dispatch(setSelectedFilters(null)); + } else { + dispatch(setSelectedFilters(getFiltersFromMap(filterMap))); + } + }, + [dispatch, history, pathname, search] + ); + + return [useGetUrlParams, updateUrlParams]; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/render_app.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/render_app.tsx index c4b05ea7a2e0c..76b86c0b82383 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/render_app.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/render_app.tsx @@ -7,9 +7,17 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { i18n as i18nFormatter } from '@kbn/i18n'; import { AppMountParameters, CoreStart } from '@kbn/core/public'; +import { SyntheticsAppProps } from './contexts'; +import { getIntegratedAppAvailability } from './utils/adapters'; +import { + DEFAULT_DARK_MODE, + DEFAULT_TIMEPICKER_QUICK_RANGES, + INTEGRATED_SOLUTIONS, +} from '../../../common/constants'; +import { SyntheticsApp } from './synthetics_app'; import { ClientPluginsSetup, ClientPluginsStart } from '../../plugin'; -import { SyntheticsApp } from '../synthetics_app'; export function renderApp( core: CoreStart, @@ -18,7 +26,56 @@ export function renderApp( appMountParameters: AppMountParameters, isDev: boolean ) { - ReactDOM.render(, appMountParameters.element); + const { + application: { capabilities }, + chrome: { setBadge, setHelpExtension }, + docLinks, + http: { basePath }, + i18n, + } = core; + + const { apm, infrastructure, logs } = getIntegratedAppAvailability( + capabilities, + INTEGRATED_SOLUTIONS + ); + + const canSave = (capabilities.uptime.save ?? false) as boolean; // TODO: Determine for synthetics + + const props: SyntheticsAppProps = { + isDev, + plugins, + canSave, + core, + i18n, + startPlugins, + basePath: basePath.get(), + darkMode: core.uiSettings.get(DEFAULT_DARK_MODE), + commonlyUsedRanges: core.uiSettings.get(DEFAULT_TIMEPICKER_QUICK_RANGES), + isApmAvailable: apm, + isInfraAvailable: infrastructure, + isLogsAvailable: logs, + renderGlobalHelpControls: () => + setHelpExtension({ + appName: i18nFormatter.translate('xpack.synthetics.header.appName', { + defaultMessage: 'Synthetics', + }), + links: [ + { + linkType: 'documentation', + href: `${docLinks.links.observability.monitorUptime}`, // TODO: Include synthetics link + }, + { + linkType: 'discuss', + href: 'https://discuss.elastic.co/c/uptime', // TODO: Include synthetics link + }, + ], + }), + setBadge, + appMountParameters, + setBreadcrumbs: core.chrome.setBreadcrumbs, + }; + + ReactDOM.render(, appMountParameters.element); return () => { ReactDOM.unmountComponentAtNode(appMountParameters.element); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx new file mode 100644 index 0000000000000..7f04b3992885b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx @@ -0,0 +1,184 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useEffect } from 'react'; +import { EuiPageTemplateProps, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Route, Switch } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { APP_WRAPPER_CLASS } from '@kbn/core/public'; +import { useInspectorContext } from '@kbn/observability-plugin/public'; +import { MonitorAddEditPage } from './components/monitor_add_edit/monitor_add_edit_page'; +import { OverviewPage } from './components/overview/overview_page'; +import { SyntheticsPageTemplateComponent } from './components/common/pages/synthetics_page_template'; +import { NotFoundPage } from './components/common/pages/not_found'; +import { ServiceAllowedWrapper } from './components/common/wrappers/service_allowed_wrapper'; +import { + MONITOR_ADD_ROUTE, + MONITOR_MANAGEMENT_ROUTE, + OVERVIEW_ROUTE, +} from '../../../common/constants'; +import { MonitorManagementPage } from './components/monitor_management/monitor_management_page'; +import { apiService } from '../../utils/api_service'; +import { SyntheticsPage, useSyntheticsTelemetry } from './hooks/use_telemetry'; + +type RouteProps = { + path: string; + component: React.FC; + dataTestSubj: string; + title: string; + telemetryId: SyntheticsPage; + pageHeader: { + pageTitle: string | JSX.Element; + children?: JSX.Element; + rightSideItems?: JSX.Element[]; + }; +} & EuiPageTemplateProps; + +const baseTitle = i18n.translate('xpack.synthetics.routes.baseTitle', { + defaultMessage: 'Synthetics - Kibana', +}); + +export const MONITOR_MANAGEMENT_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.heading', + { + defaultMessage: 'Monitor Management', + } +); + +const getRoutes = (): RouteProps[] => { + return [ + { + title: i18n.translate('xpack.synthetics.overviewRoute.title', { + defaultMessage: 'Synthetics Overview | {baseTitle}', + values: { baseTitle }, + }), + path: OVERVIEW_ROUTE, + component: () => , + dataTestSubj: 'syntheticsOverviewPage', + telemetryId: SyntheticsPage.Overview, + pageHeader: { + pageTitle: ( + + + + + + ), + rightSideItems: [ + /* */ + ], + }, + }, + { + title: i18n.translate('xpack.synthetics.monitorManagementRoute.title', { + defaultMessage: 'Monitor Management | {baseTitle}', + values: { baseTitle }, + }), + path: MONITOR_MANAGEMENT_ROUTE, + component: () => ( + + + + ), + dataTestSubj: 'syntheticsMonitorManagementPage', + telemetryId: SyntheticsPage.MonitorManagement, + pageHeader: { + pageTitle: ( + + + + + + ), + rightSideItems: [ + /* */ + ], + }, + }, + { + title: i18n.translate('xpack.synthetics.addMonitorRoute.title', { + defaultMessage: 'Add Monitor | {baseTitle}', + values: { baseTitle }, + }), + path: MONITOR_ADD_ROUTE, + component: () => ( + + + + ), + dataTestSubj: 'syntheticsMonitorAddPage', + telemetryId: SyntheticsPage.MonitorAdd, + pageHeader: { + pageTitle: ( + + ), + }, + // bottomBar: , + bottomBarProps: { paddingSize: 'm' as const }, + }, + ]; +}; + +const RouteInit: React.FC> = ({ + path, + title, + telemetryId, +}) => { + useSyntheticsTelemetry(telemetryId); + useEffect(() => { + document.title = title; + }, [path, title]); + return null; +}; + +export const PageRouter: FC = () => { + const routes = getRoutes(); + const { addInspectorRequest } = useInspectorContext(); + + apiService.addInspectorRequest = addInspectorRequest; + + return ( + + {routes.map( + ({ + title, + path, + component: RouteComponent, + dataTestSubj, + telemetryId, + pageHeader, + ...pageTemplateProps + }) => ( + +
+ {/* TODO: See if the callout is needed for Synthetics App as well */} + + + + +
+
+ ) + )} + +
+ ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/index.ts new file mode 100644 index 0000000000000..153eab7415cd5 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { store, storage } from './store'; + +export type { RooState as AppState } from './root_reducer'; + +export * from './ui'; +export * from './index_status'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/actions.ts new file mode 100644 index 0000000000000..0b359a52502fa --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/actions.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 { IHttpFetchError } from '@kbn/core/public'; +import { createAction } from '@reduxjs/toolkit'; +import { StatesIndexStatus } from '../../../../../common/runtime_types'; + +export const getIndexStatus = createAction('[INDEX STATUS] GET'); +export const getIndexStatusSuccess = createAction('[INDEX STATUS] GET SUCCESS'); +export const getIndexStatusFail = createAction('[INDEX STATUS] GET FAIL'); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/api.ts new file mode 100644 index 0000000000000..ba6ded899f9c4 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/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 { API_URLS } from '../../../../../common/constants'; +import { StatesIndexStatus, StatesIndexStatusType } from '../../../../../common/runtime_types'; +import { apiService } from '../../../../utils/api_service'; + +export const fetchIndexStatus = async (): Promise => { + return await apiService.get(API_URLS.INDEX_STATUS, undefined, StatesIndexStatusType); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/effects.ts new file mode 100644 index 0000000000000..cf0eaf83e5c2c --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/effects.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 { takeLeading } from 'redux-saga/effects'; +import { getIndexStatus, getIndexStatusSuccess, getIndexStatusFail } from './actions'; +import { fetchEffectFactory } from '../utils/fetch_effect'; +import { fetchIndexStatus } from './api'; + +export function* fetchIndexStatusEffect() { + yield takeLeading( + getIndexStatus, + fetchEffectFactory(fetchIndexStatus, getIndexStatusSuccess, getIndexStatusFail) + ); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/index.ts new file mode 100644 index 0000000000000..e140145bb324a --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/index.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 { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; +import { createReducer } from '@reduxjs/toolkit'; +import { StatesIndexStatus } from '../../../../../common/runtime_types'; + +import { getIndexStatus, getIndexStatusSuccess, getIndexStatusFail } from './actions'; + +export interface IndexStatusState { + data: StatesIndexStatus | null; + loading: boolean; + error: IHttpFetchError | null; +} + +const initialState: IndexStatusState = { + data: null, + loading: false, + error: null, +}; + +export const indexStatusReducer = createReducer(initialState, (builder) => { + builder + .addCase(getIndexStatus, (state) => { + state.loading = true; + }) + .addCase(getIndexStatusSuccess, (state, action) => { + state.data = action.payload; + state.loading = false; + }) + .addCase(getIndexStatusFail, (state, action) => { + state.error = action.payload as IHttpFetchError; + state.loading = false; + }); +}); + +export * from './actions'; +export * from './effects'; +export * from './selectors'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/selectors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/selectors.ts new file mode 100644 index 0000000000000..7d7da80b692a2 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/selectors.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createSelector } from 'reselect'; +import type { RooState } from '../root_reducer'; + +const getState = (appState: RooState) => appState.indexStatus; +export const selectIndexState = createSelector(getState, (state) => state); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts new file mode 100644 index 0000000000000..7d1aaa60aa4f3 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.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 { all, fork } from 'redux-saga/effects'; +import { fetchIndexStatusEffect } from './index_status'; + +export const rootEffect = function* root(): Generator { + yield all([fork(fetchIndexStatusEffect)]); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts new file mode 100644 index 0000000000000..a57c0af9e6fdb --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.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 { combineReducers } from '@reduxjs/toolkit'; + +import { uiReducer } from './ui'; +import { indexStatusReducer } from './index_status'; + +export const rootReducer = combineReducers({ + ui: uiReducer, + indexStatus: indexStatusReducer, +}); + +export type RooState = ReturnType; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/store.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/store.ts new file mode 100644 index 0000000000000..19320f0102a99 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/store.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 { configureStore } from '@reduxjs/toolkit'; +import createSagaMiddleware from 'redux-saga'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { rootEffect } from './root_effect'; +import { rootReducer } from './root_reducer'; + +const sagaMW = createSagaMiddleware(); + +export const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: false }).concat(sagaMW), + devTools: process.env.NODE_ENV !== 'production', + preloadedState: {}, + enhancers: [], +}); + +sagaMW.run(rootEffect); + +export const storage = new Storage(window.localStorage); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/actions.ts new file mode 100644 index 0000000000000..d613b3f3d3509 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/actions.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createAction } from '@reduxjs/toolkit'; + +export interface PopoverState { + id: string; + open: boolean; +} + +export const setAlertFlyoutVisible = createAction('[UI] TOGGLE ALERT FLYOUT'); + +export const setAlertFlyoutType = createAction('[UI] SET ALERT FLYOUT TYPE'); + +export const setBasePath = createAction('[UI] SET BASE PATH'); + +export const setEsKueryString = createAction('[UI] SET ES KUERY STRING'); + +export const setSearchTextAction = createAction('[UI] SET SEARCH'); + +export const toggleIntegrationsPopover = createAction( + '[UI] TOGGLE INTEGRATION POPOVER STATE' +); + +export const setSelectedMonitorId = createAction('[UI] SET MONITOR ID'); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/index.ts new file mode 100644 index 0000000000000..618e6dd524767 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/index.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createReducer } from '@reduxjs/toolkit'; + +import { + PopoverState, + toggleIntegrationsPopover, + setBasePath, + setEsKueryString, + setAlertFlyoutType, + setAlertFlyoutVisible, + setSearchTextAction, + setSelectedMonitorId, +} from './actions'; + +export interface UiState { + alertFlyoutVisible: boolean; + alertFlyoutType?: string; + basePath: string; + esKuery: string; + searchText: string; + integrationsPopoverOpen: PopoverState | null; + monitorId: string; +} + +const initialState: UiState = { + alertFlyoutVisible: false, + basePath: '', + esKuery: '', + searchText: '', + integrationsPopoverOpen: null, + monitorId: '', +}; + +export const uiReducer = createReducer(initialState, (builder) => { + builder + .addCase(toggleIntegrationsPopover, (state, action) => { + state.integrationsPopoverOpen = action.payload; + }) + .addCase(setAlertFlyoutVisible, (state, action) => { + state.alertFlyoutVisible = action.payload ?? !state.alertFlyoutVisible; + }) + .addCase(setAlertFlyoutType, (state, action) => { + state.alertFlyoutType = action.payload; + }) + .addCase(setBasePath, (state, action) => { + state.basePath = action.payload; + }) + .addCase(setEsKueryString, (state, action) => { + state.esKuery = action.payload; + }) + .addCase(setSearchTextAction, (state, action) => { + state.searchText = action.payload; + }) + .addCase(setSelectedMonitorId, (state, action) => { + state.monitorId = action.payload; + }); +}); + +export * from './actions'; +export * from './selectors'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/selectors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/selectors.ts new file mode 100644 index 0000000000000..52d9841cde961 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/selectors.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 { createSelector } from 'reselect'; +import type { RooState } from '../root_reducer'; + +const uiStateSelector = (appState: RooState) => appState.ui; +export const selectBasePath = createSelector(uiStateSelector, ({ basePath }) => basePath); + +export const selectIsIntegrationsPopupOpen = createSelector( + uiStateSelector, + ({ integrationsPopoverOpen }) => integrationsPopoverOpen +); + +export const selectAlertFlyoutVisibility = createSelector( + uiStateSelector, + ({ alertFlyoutVisible }) => alertFlyoutVisible +); + +export const selectAlertFlyoutType = createSelector( + uiStateSelector, + ({ alertFlyoutType }) => alertFlyoutType +); + +export const selectEsKuery = createSelector(uiStateSelector, ({ esKuery }) => esKuery); + +export const selectSearchText = createSelector(uiStateSelector, ({ searchText }) => searchText); + +export const selectMonitorId = createSelector(uiStateSelector, ({ monitorId }) => monitorId); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/fetch_effect.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/fetch_effect.ts new file mode 100644 index 0000000000000..387ceeb56a514 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/fetch_effect.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { call, put } from 'redux-saga/effects'; +import { Action } from 'redux-actions'; +import { IHttpFetchError } from '@kbn/core/public'; + +/** + * Factory function for a fetch effect. It expects three action creators, + * one to call for a fetch, one to call for success, and one to handle failures. + * @param fetch creates a fetch action + * @param success creates a success action + * @param fail creates a failure action + * @template T the action type expected by the fetch action + * @template R the type that the API request should return on success + * @template S the type of the success action + * @template F the type of the failure action + */ +export function fetchEffectFactory( + fetch: (request: T) => Promise, + success: (response: R) => Action, + fail: (error: IHttpFetchError) => Action +) { + return function* (action: Action): Generator { + try { + const response = yield call(fetch, action.payload); + if (response instanceof Error) { + // eslint-disable-next-line no-console + console.error(response); + + yield put(fail(response as IHttpFetchError)); + } else { + yield put(success(response as R)); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + yield put(fail(error as IHttpFetchError)); + } + }; +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx new file mode 100644 index 0000000000000..07fb3604abd42 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useEffect } from 'react'; +import { Provider as ReduxProvider } from 'react-redux'; +import { Router } from 'react-router-dom'; +import { EuiErrorBoundary } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { APP_WRAPPER_CLASS } from '@kbn/core/public'; +import { + KibanaContextProvider, + KibanaThemeProvider, + RedirectAppLinks, +} from '@kbn/kibana-react-plugin/public'; +import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; +import { InspectorContextProvider } from '@kbn/observability-plugin/public'; +import { SyntheticsAppProps } from './contexts'; + +import { + SyntheticsRefreshContextProvider, + SyntheticsSettingsContextProvider, + SyntheticsThemeContextProvider, + SyntheticsStartupPluginsContextProvider, + SyntheticsDataViewContextProvider, +} from './contexts'; + +import { PageRouter } from './routes'; +import { store, storage, setBasePath } from './state'; +import { kibanaService } from '../../utils/kibana_service'; +import { ActionMenu } from './components/common/header/action_menu'; + +const Application = (props: SyntheticsAppProps) => { + const { + basePath, + canSave, + core, + darkMode, + i18n: i18nCore, + plugins, + renderGlobalHelpControls, + setBadge, + startPlugins, + appMountParameters, + } = props; + + useEffect(() => { + renderGlobalHelpControls(); + setBadge( + !canSave + ? { + text: i18n.translate('xpack.synthetics.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + tooltip: i18n.translate('xpack.synthetics.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save', + }), + iconType: 'glasses', + } + : undefined + ); + }, [canSave, renderGlobalHelpControls, setBadge]); + + kibanaService.core = core; + kibanaService.theme = props.appMountParameters.theme$; + + store.dispatch(setBasePath(basePath)); + + return ( + + + + + + + + + + + + +
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +}; + +export const SyntheticsApp = (props: SyntheticsAppProps) => ; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/adapters/capabilities_adapter.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/adapters/capabilities_adapter.ts new file mode 100644 index 0000000000000..c00be7d8b0ca0 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/adapters/capabilities_adapter.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +interface IntegratedAppsAvailability { + [key: string]: boolean; +} + +export const getIntegratedAppAvailability = ( + capabilities: any, + integratedApps: string[] +): IntegratedAppsAvailability => { + return integratedApps.reduce((supportedSolutions: IntegratedAppsAvailability, solutionName) => { + supportedSolutions[solutionName] = + capabilities[solutionName] && capabilities[solutionName].show === true; + return supportedSolutions; + }, {}); +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/adapters/index.ts similarity index 69% rename from x-pack/plugins/security_solution/public/overview/components/detection_response/index.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/utils/adapters/index.ts index 8ab93de825305..2d0e4b7b2ba6f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/adapters/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { HostAlertsTable } from './host_alerts_table'; -export { UserAlertsTable } from './user_alerts_table'; +export * from './capabilities_adapter'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts new file mode 100644 index 0000000000000..adf2a15e70fa6 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.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 { AppState } from '../../../state'; + +/** + * NOTE: This variable name MUST start with 'mock*' in order for + * Jest to accept its use within a jest.mock() + */ +export const mockState: AppState = { + ui: { + alertFlyoutVisible: false, + basePath: 'yyz', + esKuery: '', + integrationsPopoverOpen: null, + searchText: '', + monitorId: '', + }, + indexStatus: { + data: null, + error: null, + loading: false, + }, +}; + +// TODO: Complete mock state diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_plugin_start_mock.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_plugin_start_mock.ts new file mode 100644 index 0000000000000..981e6a5e2ae3b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_plugin_start_mock.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +interface InputTimeRange { + from: string; + to: string; +} + +export const startPlugins = { + data: { + query: { + timefilter: { + timefilter: { + getTime: () => ({ to: 'now-15m', from: 'now-30m' }), + setTime: jest.fn(({ from, to }: InputTimeRange) => {}), + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/get_supported_url_params.test.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/get_supported_url_params.test.ts new file mode 100644 index 0000000000000..89419ca61ed43 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/get_supported_url_params.test.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import DateMath from '@kbn/datemath'; +import { getSupportedUrlParams } from '../url_params'; +import { CLIENT_DEFAULTS } from '../../../../../common/constants'; + +describe('getSupportedUrlParams', () => { + let dateMathSpy: any; + const MOCK_DATE_VALUE = 20; + + beforeEach(() => { + dateMathSpy = jest.spyOn(DateMath, 'parse'); + dateMathSpy.mockReturnValue(MOCK_DATE_VALUE); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('returns custom values', () => { + const customValues = { + autorefreshInterval: '23', + autorefreshIsPaused: 'false', + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + monitorListPageIndex: '23', + monitorListPageSize: '50', + monitorListSortDirection: 'desc', + monitorListSortField: 'monitor.status', + search: 'monitor.status: down', + selectedPingStatus: 'up', + }; + + const expected = { + absoluteDateRangeEnd: 20, + absoluteDateRangeStart: 20, + autorefreshInterval: 23, + autorefreshIsPaused: false, + dateRangeEnd: 'now', + dateRangeStart: 'now-15m', + search: 'monitor.status: down', + }; + + const result = getSupportedUrlParams(customValues); + expect(result).toMatchObject(expected); + }); + + it('returns default values', () => { + const { + AUTOREFRESH_INTERVAL, + AUTOREFRESH_IS_PAUSED, + DATE_RANGE_START, + DATE_RANGE_END, + FILTERS, + SEARCH, + STATUS_FILTER, + } = CLIENT_DEFAULTS; + const result = getSupportedUrlParams({}); + expect(result).toEqual({ + absoluteDateRangeStart: MOCK_DATE_VALUE, + absoluteDateRangeEnd: MOCK_DATE_VALUE, + autorefreshInterval: AUTOREFRESH_INTERVAL, + autorefreshIsPaused: AUTOREFRESH_IS_PAUSED, + dateRangeStart: DATE_RANGE_START, + dateRangeEnd: DATE_RANGE_END, + excludedFilters: '', + filters: FILTERS, + focusConnectorField: false, + pagination: undefined, + search: SEARCH, + statusFilter: STATUS_FILTER, + query: '', + }); + }); + + it('returns the first item for string arrays', () => { + const result = getSupportedUrlParams({ + dateRangeStart: ['now-18d', 'now-11d', 'now-5m'], + }); + + const expected = { + absoluteDateRangeEnd: 20, + absoluteDateRangeStart: 20, + autorefreshInterval: 60000, + }; + + expect(result).toMatchObject(expected); + }); + + it('provides defaults for undefined values', () => { + const result = getSupportedUrlParams({ + dateRangeStart: undefined, + }); + + const expected = { + absoluteDateRangeStart: 20, + }; + + expect(result).toMatchObject(expected); + }); + + it('provides defaults for empty string array values', () => { + const result = getSupportedUrlParams({ + dateRangeStart: [], + }); + + const expected = { + absoluteDateRangeStart: 20, + }; + + expect(result).toMatchObject(expected); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/get_supported_url_params.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/get_supported_url_params.ts new file mode 100644 index 0000000000000..c9badbcf7f728 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/get_supported_url_params.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 { parseIsPaused } from '../url_params/parse_is_paused'; +import { parseUrlInt } from '../url_params/parse_url_int'; +import { CLIENT_DEFAULTS } from '../../../../../common/constants'; +import { parseAbsoluteDate } from '../url_params/parse_absolute_date'; + +export interface SyntheticsUrlParams { + absoluteDateRangeStart: number; + absoluteDateRangeEnd: number; + autorefreshInterval: number; + autorefreshIsPaused: boolean; + dateRangeStart: string; + dateRangeEnd: string; + pagination?: string; + filters: string; + excludedFilters: string; + search: string; + statusFilter: string; + focusConnectorField?: boolean; + query?: string; +} + +const { + ABSOLUTE_DATE_RANGE_START, + ABSOLUTE_DATE_RANGE_END, + AUTOREFRESH_INTERVAL, + AUTOREFRESH_IS_PAUSED, + DATE_RANGE_START, + DATE_RANGE_END, + SEARCH, + FILTERS, + STATUS_FILTER, +} = CLIENT_DEFAULTS; + +/** + * Gets the current URL values for the application. If no item is present + * for the URL, a default value is supplied. + * + * @param params A set of key-value pairs where the value is either + * undefined or a string/string array. If a string array is passed, + * only the first item is chosen. Support for lists in the URL will + * require further development. + */ +export const getSupportedUrlParams = (params: { + [key: string]: string | string[] | undefined | null; +}): SyntheticsUrlParams => { + const filteredParams: { [key: string]: string | undefined } = {}; + Object.keys(params).forEach((key) => { + let value: string | undefined; + if (params[key] === undefined) { + value = undefined; + } else if (Array.isArray(params[key])) { + // @ts-ignore this must be an array, and it's ok if the + // 0th element is undefined + value = params[key][0]; + } else { + // @ts-ignore this will not be an array because the preceding + // block tests for that + value = params[key]; + } + filteredParams[key] = value; + }); + + const { + autorefreshInterval, + autorefreshIsPaused, + dateRangeStart, + dateRangeEnd, + filters, + excludedFilters, + search, + statusFilter, + pagination, + focusConnectorField, + query, + } = filteredParams; + + return { + pagination, + absoluteDateRangeStart: parseAbsoluteDate( + dateRangeStart || DATE_RANGE_START, + ABSOLUTE_DATE_RANGE_START + ), + absoluteDateRangeEnd: parseAbsoluteDate( + dateRangeEnd || DATE_RANGE_END, + ABSOLUTE_DATE_RANGE_END, + { roundUp: true } + ), + autorefreshInterval: parseUrlInt(autorefreshInterval, AUTOREFRESH_INTERVAL), + autorefreshIsPaused: parseIsPaused(autorefreshIsPaused, AUTOREFRESH_IS_PAUSED), + dateRangeStart: dateRangeStart || DATE_RANGE_START, + dateRangeEnd: dateRangeEnd || DATE_RANGE_END, + filters: filters || FILTERS, + excludedFilters: excludedFilters || '', + search: search || SEARCH, + statusFilter: statusFilter || STATUS_FILTER, + focusConnectorField: !!focusConnectorField, + query: query || '', + }; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/helper_with_redux.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/helper_with_redux.tsx new file mode 100644 index 0000000000000..23831b1a4bc25 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/helper_with_redux.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 type { Store } from 'redux'; +import { createStore as createReduxStore, applyMiddleware } from 'redux'; + +import { Provider as ReduxProvider } from 'react-redux'; +import createSagaMiddleware from 'redux-saga'; + +import { AppState } from '../../state'; +import { rootReducer } from '../../state/root_reducer'; +import { rootEffect } from '../../state/root_effect'; + +const createRealStore = (): Store => { + const sagaMW = createSagaMiddleware(); + const store = createReduxStore(rootReducer, applyMiddleware(sagaMW)); + sagaMW.run(rootEffect); + return store; +}; + +export const MountWithReduxProvider: React.FC<{ state?: AppState; useRealStore?: boolean }> = ({ + children, + state, + useRealStore, +}) => { + const store = useRealStore + ? createRealStore() + : { + dispatch: jest.fn(), + getState: jest.fn().mockReturnValue(state || { selectedFilters: null }), + subscribe: jest.fn(), + replaceReducer: jest.fn(), + [Symbol.observable]: jest.fn(), + }; + + return {children}; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/index.ts new file mode 100644 index 0000000000000..cebc9f5030293 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './rtl_helpers'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx new file mode 100644 index 0000000000000..71d86cc53a76b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx @@ -0,0 +1,363 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, { ReactElement, ReactNode } from 'react'; +import { of } from 'rxjs'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { + render as reactTestLibRender, + MatcherFunction, + RenderOptions, +} from '@testing-library/react'; +import { Router, Route } from 'react-router-dom'; +import { merge } from 'lodash'; +import { createMemoryHistory, History } from 'history'; +import { CoreStart } from '@kbn/core/public'; +import { I18nProvider } from '@kbn/i18n-react'; +import { EuiPageTemplate } from '@elastic/eui'; +import { coreMock } from '@kbn/core/public/mocks'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { configure } from '@testing-library/dom'; +import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; +import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; +import { KibanaContextProvider, KibanaServices } from '@kbn/kibana-react-plugin/public'; +import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { mockState } from './__mocks__/syncthetics_store.mock'; +import { MountWithReduxProvider } from './helper_with_redux'; +import { AppState } from '../../state'; +import { stringifyUrlParams } from '../url_params'; +import { ClientPluginsStart } from '../../../../plugin'; +import { + SyntheticsRefreshContextProvider, + SyntheticsStartupPluginsContextProvider, +} from '../../contexts'; +import { kibanaService } from '../../../../utils/kibana_service'; + +type DeepPartial = { + [P in keyof T]?: DeepPartial; +}; + +interface KibanaProps { + services?: KibanaServices; +} + +export interface KibanaProviderOptions { + core?: DeepPartial & Partial; + kibanaProps?: KibanaProps; +} + +interface MockKibanaProviderProps extends KibanaProviderOptions { + children: ReactElement | ReactNode; +} + +interface MockRouterProps extends MockKibanaProviderProps { + history?: History; + path?: string; +} + +type Url = + | string + | { + path: string; + queryParams: Record; + }; + +interface RenderRouterOptions extends KibanaProviderOptions { + history?: History; + renderOptions?: Omit; + state?: Partial | DeepPartial; + url?: Url; + path?: string; +} + +function getSetting(key: string): T { + return 'MMM D, YYYY @ HH:mm:ss.SSS' as unknown as T; +} + +function setSetting$(key: string): T { + return of('MMM D, YYYY @ HH:mm:ss.SSS') as unknown as T; +} + +const createMockStore = () => { + let store: Record = {}; + return { + get: jest.fn().mockImplementation((key) => store[key]), + set: jest.fn().mockImplementation((key, value) => (store[key] = value)), + remove: jest.fn().mockImplementation((key: string) => delete store[key]), + clear: jest.fn().mockImplementation(() => (store = {})), + }; +}; + +const mockAppUrls: Record = { + uptime: '/app/uptime', + observability: '/app/observability', + '/home#/tutorial/uptimeMonitors': '/home#/tutorial/uptimeMonitors', +}; + +/* default mock core */ +export const defaultCore = coreMock.createStart(); +export const mockCore: () => Partial = () => { + const core: Partial = { + ...defaultCore, + application: { + ...defaultCore.application, + getUrlForApp: (app: string) => mockAppUrls[app], + navigateToUrl: jest.fn(), + capabilities: { + ...defaultCore.application.capabilities, + uptime: { + 'alerting:save': true, + configureSettings: true, + save: true, + show: true, + }, + actions: { + save: true, + }, + }, + }, + uiSettings: { + ...defaultCore.uiSettings, + get: getSetting, + get$: setSetting$, + }, + usageCollection: { + reportUiCounter: () => {}, + }, + triggersActionsUi: triggersActionsUiMock.createStart(), + storage: createMockStore(), + data: dataPluginMock.createStartContract(), + observability: { + useRulesLink: () => ({ href: 'newRuleLink' }), + navigation: { + // @ts-ignore + PageTemplate: EuiPageTemplate, + }, + ExploratoryViewEmbeddable: () =>
Embeddable exploratory view
, + }, + }; + + return core; +}; + +/* Mock Provider Components */ +export function MockKibanaProvider({ + children, + core, + kibanaProps, +}: MockKibanaProviderProps) { + const coreOptions = merge({}, mockCore(), core); + + kibanaService.core = coreOptions as any; + + return ( + + + + + {children} + + + + + ); +} + +export function MockRouter({ + children, + core, + path, + history = createMemoryHistory(), + kibanaProps, +}: MockRouterProps) { + return ( + + + {children} + + + ); +} +configure({ testIdAttribute: 'data-test-subj' }); + +export const MockRedux = ({ + state, + history = createMemoryHistory(), + children, + path, +}: { + state: Partial; + history?: History; + children: React.ReactNode; + path?: string; + useRealStore?: boolean; +}) => { + const testState: AppState = { + ...mockState, + ...state, + }; + + return ( + + + {children} + + + ); +}; + +export function WrappedHelper({ + children, + core, + kibanaProps, + state, + url, + useRealStore, + path, + history = createMemoryHistory(), +}: RenderRouterOptions & { children: ReactElement; useRealStore?: boolean }) { + const testState: AppState = merge({}, mockState, state); + + return ( + + + {children} + + + ); +} + +/* Custom react testing library render */ +export function render( + ui: ReactElement, + { + history = createMemoryHistory(), + core, + kibanaProps, + renderOptions, + state, + url, + path, + useRealStore, + }: RenderRouterOptions & { useRealStore?: boolean } = {} +) { + if (url) { + history = getHistoryFromUrl(url); + } + + return { + ...reactTestLibRender( + + {ui} + , + renderOptions + ), + history, + }; +} + +const getHistoryFromUrl = (url: Url) => { + if (typeof url === 'string') { + return createMemoryHistory({ + initialEntries: [url], + }); + } + + return createMemoryHistory({ + initialEntries: [url.path + stringifyUrlParams(url.queryParams)], + }); +}; + +const forNearestTag = + (tag: string) => + (getByText: (f: MatcherFunction) => HTMLElement | null) => + (text: string): HTMLElement | null => + getByText((_content: string, node: Element | null) => { + if (!node) return false; + const noOtherButtonHasText = Array.from(node.children).every( + (child) => child && (child.textContent !== text || child.tagName.toLowerCase() !== tag) + ); + return ( + noOtherButtonHasText && node.textContent === text && node.tagName.toLowerCase() === tag + ); + }); + +// This function allows us to query for the nearest button with test +// no matter whether it has nested tags or not (as EuiButton elements do). +export const forNearestButton = forNearestTag('button'); + +export const forNearestAnchor = forNearestTag('a'); + +export const makeSyntheticsPermissionsCore = ( + permissions: Partial<{ + 'alerting:save': boolean; + configureSettings: boolean; + save: boolean; + show: boolean; + }> +) => { + return { + application: { + capabilities: { + uptime: { + 'alerting:save': true, + configureSettings: true, + save: true, + show: true, + ...permissions, + }, + }, + }, + }; +}; + +// This function filters out the queried elements which appear only +// either on mobile or desktop. +// +// It does so by filtering those with the class passed as the `classWrapper`. +// For mobile, we filter classes which tell elements to be hidden on desktop. +// For desktop, we do the opposite. +// +// We have this function because EUI will manipulate the visibility of some +// elements through pure CSS, which we can't assert on tests. Therefore, +// we look for the corresponding class wrapper. +const finderWithClassWrapper = + (classWrapper: string) => + ( + getterFn: (f: MatcherFunction) => HTMLElement | null, + customAttribute?: keyof Element | keyof HTMLElement + ) => + (text: string): HTMLElement | null => + getterFn((_content: string, node: Element | null) => { + if (!node) return false; + // There are actually properties that are not in Element but which + // appear on the `node`, so we must cast the customAttribute as a keyof Element + const content = node[(customAttribute as keyof Element) ?? 'innerHTML']; + if (content === text && wrappedInClass(node, classWrapper)) return true; + return false; + }); + +const wrappedInClass = (element: HTMLElement | Element, classWrapper: string): boolean => { + if (element.className.includes(classWrapper)) return true; + if (element.parentElement) return wrappedInClass(element.parentElement, classWrapper); + return false; +}; + +export const forMobileOnly = finderWithClassWrapper('hideForDesktop'); +export const forDesktopOnly = finderWithClassWrapper('hideForMobile'); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.test.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.test.ts new file mode 100644 index 0000000000000..5e67d8f0e6026 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.test.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import DateMath from '@kbn/datemath'; +import { getSupportedUrlParams } from './get_supported_url_params'; +import { CLIENT_DEFAULTS } from '../../../../../common/constants'; + +describe('getSupportedUrlParams', () => { + let dateMathSpy: any; + const MOCK_DATE_VALUE = 20; + + beforeEach(() => { + dateMathSpy = jest.spyOn(DateMath, 'parse'); + dateMathSpy.mockReturnValue(MOCK_DATE_VALUE); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('returns custom values', () => { + const customValues = { + autorefreshInterval: '23', + autorefreshIsPaused: 'false', + dateRangeStart: 'now-15m', + dateRangeEnd: 'now', + monitorListPageIndex: '23', + monitorListPageSize: '50', + monitorListSortDirection: 'desc', + monitorListSortField: 'monitor.status', + search: 'monitor.status: down', + selectedPingStatus: 'up', + }; + + const expected = { + absoluteDateRangeEnd: 20, + absoluteDateRangeStart: 20, + autorefreshInterval: 23, + autorefreshIsPaused: false, + dateRangeEnd: 'now', + dateRangeStart: 'now-15m', + search: 'monitor.status: down', + }; + + const result = getSupportedUrlParams(customValues); + expect(result).toMatchObject(expected); + }); + + it('returns default values', () => { + const { + AUTOREFRESH_INTERVAL, + AUTOREFRESH_IS_PAUSED, + DATE_RANGE_START, + DATE_RANGE_END, + FILTERS, + SEARCH, + STATUS_FILTER, + } = CLIENT_DEFAULTS; + const result = getSupportedUrlParams({}); + expect(result).toEqual({ + absoluteDateRangeStart: MOCK_DATE_VALUE, + absoluteDateRangeEnd: MOCK_DATE_VALUE, + autorefreshInterval: AUTOREFRESH_INTERVAL, + autorefreshIsPaused: AUTOREFRESH_IS_PAUSED, + dateRangeStart: DATE_RANGE_START, + dateRangeEnd: DATE_RANGE_END, + excludedFilters: '', + filters: FILTERS, + focusConnectorField: false, + pagination: undefined, + search: SEARCH, + statusFilter: STATUS_FILTER, + query: '', + }); + }); + + it('returns the first item for string arrays', () => { + const result = getSupportedUrlParams({ + dateRangeStart: ['now-18d', 'now-11d', 'now-5m'], + }); + + const expected = { + absoluteDateRangeEnd: 20, + absoluteDateRangeStart: 20, + autorefreshInterval: 60000, + }; + + expect(result).toMatchObject(expected); + }); + + it('provides defaults for undefined values', () => { + const result = getSupportedUrlParams({ + dateRangeStart: undefined, + }); + + const expected = { + absoluteDateRangeStart: 20, + }; + + expect(result).toMatchObject(expected); + }); + + it('provides defaults for empty string array values', () => { + const result = getSupportedUrlParams({ + dateRangeStart: [], + }); + + const expected = { + absoluteDateRangeStart: 20, + }; + + expect(result).toMatchObject(expected); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts new file mode 100644 index 0000000000000..1036c07973b27 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/get_supported_url_params.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { parseIsPaused } from './parse_is_paused'; +import { parseUrlInt } from './parse_url_int'; +import { CLIENT_DEFAULTS } from '../../../../../common/constants'; +import { parseAbsoluteDate } from './parse_absolute_date'; + +// TODO: Change for Synthetics App if needed (Copied from legacy_uptime) +export interface SyntheticsUrlParams { + absoluteDateRangeStart: number; + absoluteDateRangeEnd: number; + autorefreshInterval: number; + autorefreshIsPaused: boolean; + dateRangeStart: string; + dateRangeEnd: string; + pagination?: string; + filters: string; + excludedFilters: string; + search: string; + statusFilter: string; + focusConnectorField?: boolean; + query?: string; +} + +const { + ABSOLUTE_DATE_RANGE_START, + ABSOLUTE_DATE_RANGE_END, + AUTOREFRESH_INTERVAL, + AUTOREFRESH_IS_PAUSED, + DATE_RANGE_START, + DATE_RANGE_END, + SEARCH, + FILTERS, + STATUS_FILTER, +} = CLIENT_DEFAULTS; + +/** + * Gets the current URL values for the application. If no item is present + * for the URL, a default value is supplied. + * + * @param params A set of key-value pairs where the value is either + * undefined or a string/string array. If a string array is passed, + * only the first item is chosen. Support for lists in the URL will + * require further development. + */ +export const getSupportedUrlParams = (params: { + [key: string]: string | string[] | undefined | null; +}): SyntheticsUrlParams => { + const filteredParams: { [key: string]: string | undefined } = {}; + Object.keys(params).forEach((key) => { + let value: string | undefined; + if (params[key] === undefined) { + value = undefined; + } else if (Array.isArray(params[key])) { + // @ts-ignore this must be an array, and it's ok if the + // 0th element is undefined + value = params[key][0]; + } else { + // @ts-ignore this will not be an array because the preceding + // block tests for that + value = params[key]; + } + filteredParams[key] = value; + }); + + const { + autorefreshInterval, + autorefreshIsPaused, + dateRangeStart, + dateRangeEnd, + filters, + excludedFilters, + search, + statusFilter, + pagination, + focusConnectorField, + query, + } = filteredParams; + + return { + pagination, + absoluteDateRangeStart: parseAbsoluteDate( + dateRangeStart || DATE_RANGE_START, + ABSOLUTE_DATE_RANGE_START + ), + absoluteDateRangeEnd: parseAbsoluteDate( + dateRangeEnd || DATE_RANGE_END, + ABSOLUTE_DATE_RANGE_END, + { roundUp: true } + ), + autorefreshInterval: parseUrlInt(autorefreshInterval, AUTOREFRESH_INTERVAL), + autorefreshIsPaused: parseIsPaused(autorefreshIsPaused, AUTOREFRESH_IS_PAUSED), + dateRangeStart: dateRangeStart || DATE_RANGE_START, + dateRangeEnd: dateRangeEnd || DATE_RANGE_END, + filters: filters || FILTERS, + excludedFilters: excludedFilters || '', + search: search || SEARCH, + statusFilter: statusFilter || STATUS_FILTER, + focusConnectorField: !!focusConnectorField, + query: query || '', + }; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/index.ts new file mode 100644 index 0000000000000..a3f27112b0e81 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { SyntheticsUrlParams } from './get_supported_url_params'; +export { getSupportedUrlParams } from './get_supported_url_params'; +export * from './stringify_url_params'; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/parse_absolute_date.test.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/parse_absolute_date.test.ts similarity index 100% rename from x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/parse_absolute_date.test.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/parse_absolute_date.test.ts diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/parse_absolute_date.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/parse_absolute_date.ts similarity index 100% rename from x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/parse_absolute_date.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/parse_absolute_date.ts diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/parse_is_paused.test.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/parse_is_paused.test.ts similarity index 100% rename from x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/parse_is_paused.test.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/parse_is_paused.test.ts diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/parse_is_paused.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/parse_is_paused.ts similarity index 100% rename from x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/parse_is_paused.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/parse_is_paused.ts diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/parse_url_int.test.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/parse_url_int.test.ts similarity index 100% rename from x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/parse_url_int.test.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/parse_url_int.test.ts diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/parse_url_int.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/parse_url_int.ts similarity index 100% rename from x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/parse_url_int.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/parse_url_int.ts diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/stringify_url_params.test.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/stringify_url_params.test.ts similarity index 100% rename from x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/stringify_url_params.test.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/stringify_url_params.test.ts diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/stringify_url_params.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/stringify_url_params.ts similarity index 86% rename from x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/stringify_url_params.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/stringify_url_params.ts index 2bb5f9a3c13e3..59c71af795cce 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/stringify_url_params.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/url_params/stringify_url_params.ts @@ -6,8 +6,8 @@ */ import { stringify } from 'query-string'; -import { UptimeUrlParams } from './url_params'; -import { CLIENT_DEFAULTS } from '../../../../common/constants'; +import { SyntheticsUrlParams } from './get_supported_url_params'; +import { CLIENT_DEFAULTS } from '../../../../../common/constants'; const { AUTOREFRESH_INTERVAL, @@ -17,7 +17,7 @@ const { FOCUS_CONNECTOR_FIELD, } = CLIENT_DEFAULTS; -export const stringifyUrlParams = (params: Partial, ignoreEmpty = false) => { +export const stringifyUrlParams = (params: Partial, ignoreEmpty = false) => { if (ignoreEmpty) { // We don't want to encode this values because they are often set to Date.now(), the relative // values in dateRangeStart are better for a URL. diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/hooks/use_breakpoints.test.ts b/x-pack/plugins/synthetics/public/hooks/use_breakpoints.test.ts similarity index 100% rename from x-pack/plugins/synthetics/public/legacy_uptime/hooks/use_breakpoints.test.ts rename to x-pack/plugins/synthetics/public/hooks/use_breakpoints.test.ts diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/hooks/use_breakpoints.ts b/x-pack/plugins/synthetics/public/hooks/use_breakpoints.ts similarity index 100% rename from x-pack/plugins/synthetics/public/legacy_uptime/hooks/use_breakpoints.ts rename to x-pack/plugins/synthetics/public/hooks/use_breakpoints.ts diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/app/render_app.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/app/render_app.tsx index efb35813201ae..446a329901815 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/app/render_app.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/app/render_app.tsx @@ -55,7 +55,7 @@ export function renderApp( isLogsAvailable: logs, renderGlobalHelpControls: () => setHelpExtension({ - appName: i18nFormatter.translate('xpack.synthetics.header.appName', { + appName: i18nFormatter.translate('xpack.synthetics.legacyHeader.appName', { defaultMessage: 'Uptime', }), links: [ diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.test.tsx index 38105052f352e..4efaf26a7ac11 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.test.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.test.tsx @@ -11,9 +11,9 @@ import 'jest-styled-components'; import { render } from '../lib/helper/rtl_helpers'; import { UptimePageTemplateComponent } from './uptime_page_template'; import { OVERVIEW_ROUTE } from '../../../common/constants'; -import { useBreakpoints } from '../hooks/use_breakpoints'; +import { useBreakpoints } from '../../hooks/use_breakpoints'; -jest.mock('../hooks/use_breakpoints', () => { +jest.mock('../../hooks/use_breakpoints', () => { const down = jest.fn().mockReturnValue(false); return { useBreakpoints: () => ({ down }), diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx index 08ebd5f06a69b..fa3ad7e0805e8 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx @@ -16,7 +16,7 @@ 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'; -import { useBreakpoints } from '../hooks/use_breakpoints'; +import { useBreakpoints } from '../../hooks/use_breakpoints'; interface Props { path: string; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/common/header/action_menu_content.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/common/header/action_menu_content.tsx index 1fb150bd9e70e..e1330d9924261 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/common/header/action_menu_content.tsx @@ -17,7 +17,7 @@ import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_cont import { useGetUrlParams } from '../../../hooks'; import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; import { MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../../common/constants'; -import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params'; +import { stringifyUrlParams } from '../../../../apps/synthetics/utils/url_params/stringify_url_params'; import { InspectorHeaderLink } from './inspector_header_link'; import { monitorStatusSelector } from '../../../state/selectors'; import { ManageMonitorsBtn } from './manage_monitors_btn'; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/common/header/page_tabs.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/common/header/page_tabs.tsx index 9711a9d7acb9e..08725ac203bab 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/common/header/page_tabs.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/common/header/page_tabs.tsx @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import { useHistory, useRouteMatch } from 'react-router-dom'; import { CERTIFICATES_ROUTE, OVERVIEW_ROUTE } from '../../../../../common/constants'; import { useGetUrlParams } from '../../../hooks'; -import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params'; +import { stringifyUrlParams } from '../../../../apps/synthetics/utils/url_params/stringify_url_params'; const tabs = [ { diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx index 1003b51319136..73996c4e3a1b7 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx @@ -10,7 +10,7 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { ScreenshotRefImageData } from '../../../../../../../common/runtime_types'; -import { useBreakpoints } from '../../../../../hooks'; +import { useBreakpoints } from '../../../../../../hooks/use_breakpoints'; import { nextAriaLabel, prevAriaLabel } from './translations'; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx index fbd21fe09c1ec..4b9374e991e6b 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx @@ -27,7 +27,7 @@ import { TCPSimpleFields, } from '../../../../../common/runtime_types'; import { UptimeSettingsContext } from '../../../contexts'; -import { useBreakpoints } from '../../../hooks'; +import { useBreakpoints } from '../../../../hooks/use_breakpoints'; import { MonitorManagementList as MonitorManagementListState } from '../../../state/reducers/monitor_management'; import * as labels from '../../overview/monitor_list/translations'; import { Actions } from './actions'; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/columns/monitor_name_col.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/columns/monitor_name_col.tsx index 00590f46b51f1..0b6cc173acde1 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/columns/monitor_name_col.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/columns/monitor_name_col.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiText } from '@elastic/eui'; import { MonitorPageLink } from '../../../common/monitor_page_link'; import { useGetUrlParams } from '../../../../hooks'; -import { stringifyUrlParams } from '../../../../lib/helper/stringify_url_params'; +import { stringifyUrlParams } from '../../../../../apps/synthetics/utils/url_params/stringify_url_params'; import { MonitorSummary } from '../../../../../../common/runtime_types/monitor'; import { useFilterUpdate } from '../../../../hooks/use_filter_update'; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx index 00484b8c40070..c5cdc60253f94 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx @@ -15,7 +15,7 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { MonitorPageLink } from '../../../common/monitor_page_link'; import { useGetUrlParams } from '../../../../hooks'; -import { stringifyUrlParams } from '../../../../lib/helper/stringify_url_params'; +import { stringifyUrlParams } from '../../../../../apps/synthetics/utils/url_params/stringify_url_params'; import { PingError } from '../../../../../../common/runtime_types'; interface MostRecentErrorProps { diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/hooks/index.ts b/x-pack/plugins/synthetics/public/legacy_uptime/hooks/index.ts index b93373481f9f3..6d2a826caa4b4 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/hooks/index.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/hooks/index.ts @@ -12,5 +12,4 @@ export * from './use_search_text'; export * from './use_cert_status'; export * from './use_telemetry'; export * from './use_url_params'; -export * from './use_breakpoints'; export { useUptimeDataView } from '../contexts/uptime_data_view_context'; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/hooks/use_breadcrumbs.ts b/x-pack/plugins/synthetics/public/legacy_uptime/hooks/use_breadcrumbs.ts index 26e9d2d0fd285..0befbeb14832c 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/hooks/use_breadcrumbs.ts @@ -11,7 +11,7 @@ import { MouseEvent, useEffect } from 'react'; import { EuiBreadcrumb } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { UptimeUrlParams } from '../lib/helper'; -import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; +import { stringifyUrlParams } from '../../apps/synthetics/utils/url_params/stringify_url_params'; import { useUrlParams } from '.'; import { PLUGIN } from '../../../common/constants/plugin'; @@ -57,7 +57,7 @@ export const makeBaseBreadcrumb = ( href: observabilityPath, }, { - text: i18n.translate('xpack.synthetics.breadcrumbs.overviewBreadcrumbText', { + text: i18n.translate('xpack.synthetics.breadcrumbs.legacyOverviewBreadcrumbText', { defaultMessage: 'Uptime', }), href: uptimePath, diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/rtl_helpers.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/rtl_helpers.tsx index db55ee3805ef1..3a1fed73b90d6 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/rtl_helpers.tsx @@ -30,7 +30,7 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { mockState } from '../__mocks__/uptime_store.mock'; import { MountWithReduxProvider } from './helper_with_redux'; import { AppState } from '../../state'; -import { stringifyUrlParams } from './stringify_url_params'; +import { stringifyUrlParams } from '../../../apps/synthetics/utils/url_params/stringify_url_params'; import { ClientPluginsStart } from '../../../plugin'; import { UptimeRefreshContextProvider, UptimeStartupPluginsContextProvider } from '../../contexts'; import { kibanaService } from '../../state/kibana_service'; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/get_supported_url_params.ts b/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/get_supported_url_params.ts index 703b2257ee176..246db87b983fe 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/get_supported_url_params.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/url_params/get_supported_url_params.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { parseIsPaused } from './parse_is_paused'; -import { parseUrlInt } from './parse_url_int'; +import { parseIsPaused } from '../../../../apps/synthetics/utils/url_params/parse_is_paused'; +import { parseUrlInt } from '../../../../apps/synthetics/utils/url_params/parse_url_int'; import { CLIENT_DEFAULTS } from '../../../../../common/constants'; -import { parseAbsoluteDate } from './parse_absolute_date'; +import { parseAbsoluteDate } from '../../../../apps/synthetics/utils/url_params/parse_absolute_date'; export interface UptimeUrlParams { absoluteDateRangeStart: number; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/checks_navigation.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/checks_navigation.tsx index ee33191c3799e..f9d98b7b640c6 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/checks_navigation.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/checks_navigation.tsx @@ -12,7 +12,7 @@ import { useHistory } from 'react-router-dom'; import moment from 'moment'; import { SyntheticsJourneyApiResponse } from '../../../../common/runtime_types/ping'; import { getShortTimeStamp } from '../../components/overview/monitor_list/columns/monitor_status_column'; -import { useBreakpoints } from '../../hooks/use_breakpoints'; +import { useBreakpoints } from '../../../hooks/use_breakpoints'; interface Props { timestamp: string; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/routes.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/routes.tsx index 1b8706ad4cb00..ac9ffdd2b70e7 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/routes.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/routes.tsx @@ -72,7 +72,7 @@ type RouteProps = { }; } & EuiPageTemplateProps; -const baseTitle = i18n.translate('xpack.synthetics.routes.baseTitle', { +const baseTitle = i18n.translate('xpack.synthetics.routes.legacyBaseTitle', { defaultMessage: 'Uptime - Kibana', }); diff --git a/x-pack/plugins/synthetics/public/plugin.ts b/x-pack/plugins/synthetics/public/plugin.ts index 88238b1bfbf37..412f9167a8845 100644 --- a/x-pack/plugins/synthetics/public/plugin.ts +++ b/x-pack/plugins/synthetics/public/plugin.ts @@ -30,6 +30,7 @@ import { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/p import { FleetStart } from '@kbn/fleet-plugin/public'; import { + enableNewSyntheticsView, FetchDataParams, ObservabilityPublicSetup, ObservabilityPublicStart, @@ -123,41 +124,8 @@ export class UptimePlugin }, }); - plugins.observability.navigation.registerSections( - from(core.getStartServices()).pipe( - map(([coreStart]) => { - if (coreStart.application.capabilities.uptime.show) { - return [ - { - label: 'Uptime', - sortKey: 500, - entries: [ - { - label: i18n.translate('xpack.synthetics.overview.heading', { - defaultMessage: 'Monitors', - }), - app: 'uptime', - path: '/', - matchFullPath: true, - ignoreTrailingSlash: true, - }, - { - label: i18n.translate('xpack.synthetics.certificatesPage.heading', { - defaultMessage: 'TLS Certificates', - }), - app: 'uptime', - path: '/certificates', - matchFullPath: true, - }, - ], - }, - ]; - } - - return []; - }) - ) - ); + registerUptimeRoutesWithNavigation(core, plugins); + registerSyntheticsRoutesWithNavigation(core, plugins); const { observabilityRuleTypeRegistry } = plugins.observability; @@ -225,22 +193,26 @@ export class UptimePlugin }, }); - // Register the Synthetics UI plugin - core.application.register({ - id: 'synthetics', - euiIconType: 'logoObservability', - order: 8400, - title: PLUGIN.SYNTHETICS, - category: DEFAULT_APP_CATEGORIES.observability, - keywords: appKeywords, - deepLinks: [], - mount: async (params: AppMountParameters) => { - const [coreStart, corePlugins] = await core.getStartServices(); + const isSyntheticsViewEnabled = core.uiSettings.get(enableNewSyntheticsView); - const { renderApp } = await import('./apps/synthetics/render_app'); - return renderApp(coreStart, plugins, corePlugins, params, this.initContext.env.mode.dev); - }, - }); + if (isSyntheticsViewEnabled) { + // Register the Synthetics UI plugin + core.application.register({ + id: 'synthetics', + euiIconType: 'logoObservability', + order: 8400, + title: PLUGIN.SYNTHETICS, + category: DEFAULT_APP_CATEGORIES.observability, + keywords: appKeywords, + deepLinks: [], + mount: async (params: AppMountParameters) => { + const [coreStart, corePlugins] = await core.getStartServices(); + + const { renderApp } = await import('./apps/synthetics/render_app'); + return renderApp(coreStart, plugins, corePlugins, params, this.initContext.env.mode.dev); + }, + }); + } } public start(start: CoreStart, plugins: ClientPluginsStart): void { @@ -270,3 +242,77 @@ export class UptimePlugin public stop(): void {} } + +function registerSyntheticsRoutesWithNavigation( + core: CoreSetup, + plugins: ClientPluginsSetup +) { + plugins.observability.navigation.registerSections( + from(core.getStartServices()).pipe( + map(([coreStart]) => { + if (coreStart.application.capabilities.uptime.show) { + return [ + { + label: 'Synthetics', + sortKey: 499, + entries: [ + { + label: i18n.translate('xpack.synthetics.overview.heading', { + defaultMessage: 'Monitors', + }), + app: 'synthetics', + path: '/manage-monitors', + matchFullPath: true, + ignoreTrailingSlash: true, + }, + ], + }, + ]; + } + + return []; + }) + ) + ); +} + +function registerUptimeRoutesWithNavigation( + core: CoreSetup, + plugins: ClientPluginsSetup +) { + plugins.observability.navigation.registerSections( + from(core.getStartServices()).pipe( + map(([coreStart]) => { + if (coreStart.application.capabilities.uptime.show) { + return [ + { + label: 'Uptime', + sortKey: 500, + entries: [ + { + label: i18n.translate('xpack.synthetics.overview.heading', { + defaultMessage: 'Monitors', + }), + app: 'uptime', + path: '/', + matchFullPath: true, + ignoreTrailingSlash: true, + }, + { + label: i18n.translate('xpack.synthetics.certificatesPage.heading', { + defaultMessage: 'TLS Certificates', + }), + app: 'uptime', + path: '/certificates', + matchFullPath: true, + }, + ], + }, + ]; + } + + return []; + }) + ) + ); +} diff --git a/x-pack/plugins/synthetics/public/utils/api_service/api_service.ts b/x-pack/plugins/synthetics/public/utils/api_service/api_service.ts new file mode 100644 index 0000000000000..cbba8cf02cf22 --- /dev/null +++ b/x-pack/plugins/synthetics/public/utils/api_service/api_service.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 { isRight } from 'fp-ts/lib/Either'; +import { formatErrors } from '@kbn/securitysolution-io-ts-utils'; +import { HttpFetchQuery, HttpSetup } from '@kbn/core/public'; +import { FETCH_STATUS, AddInspectorRequest } from '@kbn/observability-plugin/public'; + +class ApiService { + private static instance: ApiService; + private _http!: HttpSetup; + private _addInspectorRequest!: AddInspectorRequest; + + public get http() { + return this._http; + } + + public set http(httpSetup: HttpSetup) { + this._http = httpSetup; + } + + public get addInspectorRequest() { + return this._addInspectorRequest; + } + + public set addInspectorRequest(addInspectorRequest: AddInspectorRequest) { + this._addInspectorRequest = addInspectorRequest; + } + + private constructor() {} + + static getInstance(): ApiService { + if (!ApiService.instance) { + ApiService.instance = new ApiService(); + } + + return ApiService.instance; + } + + public async get( + apiUrl: string, + params?: HttpFetchQuery, + decodeType?: any, + asResponse = false + ) { + const response = await this._http!.fetch({ + path: apiUrl, + query: params, + asResponse, + }); + + this.addInspectorRequest?.({ data: response, status: FETCH_STATUS.SUCCESS, loading: false }); + + if (decodeType) { + const decoded = decodeType.decode(response); + if (isRight(decoded)) { + return decoded.right as T; + } else { + // eslint-disable-next-line no-console + console.error( + `API ${apiUrl} is not returning expected response, ${formatErrors( + decoded.left + )} for response`, + response + ); + } + } + + return response; + } + + public async post(apiUrl: string, data?: any, decodeType?: any) { + const response = await this._http!.post(apiUrl, { + method: 'POST', + body: JSON.stringify(data), + }); + + this.addInspectorRequest?.({ data: response, status: FETCH_STATUS.SUCCESS, loading: false }); + + if (decodeType) { + const decoded = decodeType.decode(response); + if (isRight(decoded)) { + return decoded.right as T; + } else { + // eslint-disable-next-line no-console + console.warn( + `API ${apiUrl} is not returning expected response, ${formatErrors(decoded.left)}` + ); + } + } + return response; + } + + public async put(apiUrl: string, data?: any, decodeType?: any) { + const response = await this._http!.put(apiUrl, { + method: 'PUT', + body: JSON.stringify(data), + }); + + if (decodeType) { + const decoded = decodeType.decode(response); + if (isRight(decoded)) { + return decoded.right as T; + } else { + // eslint-disable-next-line no-console + console.warn( + `API ${apiUrl} is not returning expected response, ${formatErrors(decoded.left)}` + ); + } + } + return response; + } + + public async delete(apiUrl: string) { + const response = await this._http!.delete(apiUrl); + if (response instanceof Error) { + throw response; + } + return response; + } +} + +export const apiService = ApiService.getInstance(); diff --git a/x-pack/plugins/synthetics/public/utils/api_service/index.ts b/x-pack/plugins/synthetics/public/utils/api_service/index.ts new file mode 100644 index 0000000000000..566bde2d4b777 --- /dev/null +++ b/x-pack/plugins/synthetics/public/utils/api_service/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './api_service'; diff --git a/x-pack/plugins/synthetics/public/utils/kibana_service/index.ts b/x-pack/plugins/synthetics/public/utils/kibana_service/index.ts new file mode 100644 index 0000000000000..c623088ee9ba4 --- /dev/null +++ b/x-pack/plugins/synthetics/public/utils/kibana_service/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './kibana_service'; diff --git a/x-pack/plugins/synthetics/public/utils/kibana_service/kibana_service.ts b/x-pack/plugins/synthetics/public/utils/kibana_service/kibana_service.ts new file mode 100644 index 0000000000000..06fa2b892b4a0 --- /dev/null +++ b/x-pack/plugins/synthetics/public/utils/kibana_service/kibana_service.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 type { Observable } from 'rxjs'; +import type { CoreStart, CoreTheme } from '@kbn/core/public'; +import { apiService } from '../api_service/api_service'; + +class KibanaService { + private static instance: KibanaService; + private _core!: CoreStart; + private _theme!: Observable; + + public get core() { + return this._core; + } + + public set core(coreStart: CoreStart) { + this._core = coreStart; + apiService.http = this._core.http; + } + + public get theme() { + return this._theme; + } + + public set theme(coreTheme: Observable) { + this._theme = coreTheme; + } + + public get toasts() { + return this._core.notifications.toasts; + } + + private constructor() {} + + static getInstance(): KibanaService { + if (!KibanaService.instance) { + KibanaService.instance = new KibanaService(); + } + + return KibanaService.instance; + } +} + +export const kibanaService = KibanaService.getInstance(); diff --git a/x-pack/plugins/synthetics/scripts/e2e.js b/x-pack/plugins/synthetics/scripts/e2e.js index a329a6bf03c4b..cfdd7b09f806e 100644 --- a/x-pack/plugins/synthetics/scripts/e2e.js +++ b/x-pack/plugins/synthetics/scripts/e2e.js @@ -61,7 +61,7 @@ const config = './playwright_run.ts'; function executeRunner() { if (server) { childProcess.execSync( - `node ../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}'`, + `node ../../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}'`, { cwd: e2eDir, stdio: 'inherit', @@ -69,7 +69,7 @@ function executeRunner() { ); } else if (runner) { childProcess.execSync( - `node ../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}' --headless ${headless} --grep '${grep}'`, + `node ../../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}' --headless ${headless} --grep '${grep}'`, { cwd: e2eDir, stdio: 'inherit', @@ -77,7 +77,7 @@ function executeRunner() { ); } else { childProcess.execSync( - `node ../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}' --grep '${grep}'`, + `node ../../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}' --grep '${grep}'`, { cwd: e2eDir, stdio: 'inherit', diff --git a/x-pack/plugins/timelines/common/types/timeline/index.ts b/x-pack/plugins/timelines/common/types/timeline/index.ts index 867264fa81546..528c6e4293cf4 100644 --- a/x-pack/plugins/timelines/common/types/timeline/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/index.ts @@ -314,7 +314,7 @@ export enum TimelineId { usersPageExternalAlerts = 'users-page-external-alerts', hostsPageEvents = 'hosts-page-events', hostsPageExternalAlerts = 'hosts-page-external-alerts', - hostsPageSessions = 'hosts-page-sessions', + hostsPageSessions = 'hosts-page-sessions-v2', detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx index d6c2c7de5aa18..fd590c468b7e7 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx @@ -6,7 +6,6 @@ */ import { EuiBadge, EuiLoadingSpinner } from '@elastic/eui'; -import { pickBy } from 'lodash/fp'; import styled from 'styled-components'; import { TimelineId } from '../../../../types'; @@ -108,27 +107,46 @@ export const filterSelectedBrowserFields = ({ }): BrowserFields => { const selectedFieldIds = new Set(columnHeaders.map(({ id }) => id)); - const filteredBrowserFields: BrowserFields = Object.keys(browserFields).reduce( - (filteredCategories, categoryId) => ({ - ...filteredCategories, - [categoryId]: { - ...browserFields[categoryId], - fields: pickBy( - ({ name }) => name != null && selectedFieldIds.has(name), - browserFields[categoryId].fields - ), - }, - }), - {} - ); - - // only pick non-empty categories from the filtered browser fields - const nonEmptyCategories: BrowserFields = pickBy( - (category) => categoryHasFields(category), - filteredBrowserFields - ); - - return nonEmptyCategories; + const result: Record> = {}; + + for (const [categoryName, categoryDescriptor] of Object.entries(browserFields)) { + if (!categoryDescriptor.fields) { + // ignore any category that is missing fields. This is not expected to happen. + // eslint-disable-next-line no-continue + continue; + } + + // keep track of whether this category had a selected field, if so, we should emit it into the result + let hadSelected = false; + + // The selected fields for this `categoryName` + const selectedFields: Record> = {}; + + for (const [fieldName, fieldDescriptor] of Object.entries(categoryDescriptor.fields)) { + // For historical reasons, we consider the name as it appears on the field descriptor, not the `fieldName` (attribute name) itself. + // It is unclear if there is any point in continuing to do this. + const fieldNameFromDescriptor = fieldDescriptor.name; + + if (!fieldNameFromDescriptor) { + // Ignore any field that is missing a name in its descriptor. This is not expected to happen. + // eslint-disable-next-line no-continue + continue; + } + + if (selectedFieldIds.has(fieldNameFromDescriptor)) { + hadSelected = true; + selectedFields[fieldName] = fieldDescriptor; + } + } + + if (hadSelected) { + result[categoryName] = { + ...browserFields[categoryName], + fields: selectedFields, + }; + } + } + return result; }; export const getAlertColumnHeader = (timelineId: string, fieldId: string) => diff --git a/x-pack/plugins/timelines/public/store/t_grid/types.ts b/x-pack/plugins/timelines/public/store/t_grid/types.ts index c4627b3accd71..8e0b7e995dbcd 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/types.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/types.ts @@ -46,7 +46,7 @@ export enum TimelineId { usersPageExternalAlerts = 'users-page-external-alerts', hostsPageEvents = 'hosts-page-events', hostsPageExternalAlerts = 'hosts-page-external-alerts', - hostsPageSessions = 'hosts-page-sessions', + hostsPageSessions = 'hosts-page-sessions-v2', detectionsRulesDetailsPage = 'detections-rules-details-page', detectionsPage = 'detections-page', networkPageExternalAlerts = 'network-page-external-alerts', diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts index 980f19ac2950c..d450daadf4689 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts @@ -209,17 +209,13 @@ const timelineSessionsSearchStrategy = ({ }; const collapse = { - field: 'process.entity_id', - inner_hits: { - name: 'last_event', - size: 1, - sort: [{ '@timestamp': 'desc' }], - }, + field: 'process.entry_leader.entity_id', }; + const aggs = { total: { cardinality: { - field: 'process.entity_id', + field: 'process.entry_leader.entity_id', }, }, }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index a3f72e12dd15a..f23d9cf64202b 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -695,13 +695,10 @@ "xpack.lens.indexPattern.timeShiftSmallWarning": "{label} utilise un décalage temporel de {columnTimeShift} qui est inférieur à l'intervalle de l'histogramme des dates de {interval}. Pour éviter une non-correspondance des données, utilisez un multiple de {interval} comme décalage.", "xpack.lens.indexPattern.uniqueLabel": "{label} [{num}]", "xpack.lens.indexPattern.useAsTopLevelAgg": "Regrouper d'abord en fonction de ce champ", - "xpack.lens.indexPatterns.actionsPopoverLabel": "Paramètres Vue de données", - "xpack.lens.indexPatterns.addFieldButton": "Ajouter un champ à la vue de données", "xpack.lens.indexPatterns.clearFiltersLabel": "Effacer le nom et saisissez les filtres", "xpack.lens.indexPatterns.fieldFiltersLabel": "Filtrer par type", "xpack.lens.indexPatterns.fieldSearchLiveRegion": "{availableFields} {availableFields, plural, one {champ} other {champs}} disponible(s). {emptyFields} {emptyFields, plural, one {champ} other {champs}} vide(s). {metaFields} {metaFields, plural, one {champ} other {champs}} méta.", "xpack.lens.indexPatterns.filterByNameLabel": "Rechercher les noms de champs", - "xpack.lens.indexPatterns.manageFieldButton": "Gérer les champs de la vue de données", "xpack.lens.indexPatterns.noAvailableDataLabel": "Aucun champ disponible ne contient de données.", "xpack.lens.indexPatterns.noDataLabel": "Aucun champ.", "xpack.lens.indexPatterns.noEmptyDataLabel": "Aucun champ vide.", @@ -2721,121 +2718,6 @@ "dataViews.unableWriteLabel": "Impossible d’écrire la vue de données ! Actualisez la page pour obtenir la dernière version de cette vue de données.", "devTools.badge.betaLabel": "Bêta", "devTools.badge.betaTooltipText": "Cette fonctionnalité pourra considérablement changer dans les futures versions", - "unifiedSearch.search.unableToGetSavedQueryToastTitle": "Impossible de charger la requête enregistrée {savedQueryId}", - "unifiedSearch.noDataPopover.content": "Cette plage temporelle ne contient pas de données. Étendez ou ajustez la plage temporelle pour obtenir plus de champs et pouvoir créer des graphiques.", - "unifiedSearch.noDataPopover.dismissAction": "Ne plus afficher", - "unifiedSearch.noDataPopover.subtitle": "Conseil", - "unifiedSearch.noDataPopover.title": "Ensemble de données vide", - "unifiedSearch.query.queryBar.clearInputLabel": "Effacer l'entrée", - "unifiedSearch.query.queryBar.comboboxAriaLabel": "Rechercher et filtrer la page {pageType}", - "unifiedSearch.query.queryBar.kqlFullLanguageName": "Langage de requête Kibana", - "unifiedSearch.query.queryBar.kqlLanguageName": "KQL", - "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoDocLinkText": "documents", - "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoOptOutText": "Ne plus afficher", - "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoText": "Il semblerait que votre requête porte sur un champ imbriqué. Selon le résultat visé, il existe plusieurs façons de construire une syntaxe KQL pour des requêtes imbriquées. Apprenez-en plus avec notre {link}.", - "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoTitle": "Syntaxe de requête imbriquée KQL", - "unifiedSearch.query.queryBar.kqlOffLabel": "Désactivé", - "unifiedSearch.query.queryBar.kqlOnLabel": "Activé", - "unifiedSearch.query.queryBar.languageSwitcher.toText": "Passer au langage de requête Kibana pour la recherche", - "unifiedSearch.query.queryBar.luceneLanguageName": "Lucene", - "unifiedSearch.query.queryBar.searchInputAriaLabel": "Commencer à taper pour rechercher et filtrer la page {pageType}", - "unifiedSearch.query.queryBar.searchInputPlaceholder": "Recherche", - "unifiedSearch.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) offre une syntaxe de requête simplifiée et la prise en charge des champs scriptés. KQL offre également une fonctionnalité de saisie semi-automatique. Si vous désactivez KQL, {nonKqlModeHelpText}.", - "unifiedSearch.query.queryBar.syntaxOptionsDescription.nonKqlModeHelpText": "Kibana utilise Lucene.", - "unifiedSearch.search.searchBar.savedQueryDescriptionLabelText": "Description", - "unifiedSearch.search.searchBar.savedQueryDescriptionText": "Enregistrez le texte et les filtres de la requête que vous souhaitez réutiliser.", - "unifiedSearch.search.searchBar.savedQueryForm.titleConflictText": "Ce nom est en conflit avec une requête enregistrée existante.", - "unifiedSearch.search.searchBar.savedQueryFormCancelButtonText": "Annuler", - "unifiedSearch.search.searchBar.savedQueryFormSaveButtonText": "Enregistrer", - "unifiedSearch.search.searchBar.savedQueryFormTitle": "Enregistrer la requête", - "unifiedSearch.search.searchBar.savedQueryIncludeFiltersLabelText": "Inclure les filtres", - "unifiedSearch.search.searchBar.savedQueryIncludeTimeFilterLabelText": "Inclure le filtre temporel", - "unifiedSearch.search.searchBar.savedQueryNameHelpText": "Un nom est requis. Le nom ne peut pas contenir d'espace vide au début ou à la fin. Le nom doit être unique.", - "unifiedSearch.search.searchBar.savedQueryNameLabelText": "Nom", - "unifiedSearch.search.searchBar.savedQueryNoSavedQueriesText": "Aucune requête enregistrée.", - "unifiedSearch.search.searchBar.savedQueryPopoverButtonText": "Voir les requêtes enregistrées", - "unifiedSearch.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "Effacer la requête enregistrée en cours", - "unifiedSearch.search.searchBar.savedQueryPopoverClearButtonText": "Effacer", - "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "Annuler", - "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "Supprimer", - "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "Supprimer \"{savedQueryName}\" ?", - "unifiedSearch.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "Supprimer la requête enregistrée {savedQueryName}", - "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "Enregistrer en tant que nouvelle requête enregistrée", - "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "Enregistrer en tant que nouvelle", - "unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "Enregistrer une nouvelle requête enregistrée", - "unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonText": "Enregistrer la requête en cours", - "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "Enregistrer les modifications apportées à {title}", - "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText": "Enregistrer les modifications", - "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "Bouton de requête enregistrée {savedQueryName}", - "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "Description de {savedQueryName}", - "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "Bouton de requête enregistrée {savedQueryName} sélectionné. Appuyez pour effacer les modifications.", - "unifiedSearch.search.searchBar.savedQueryPopoverTitleText": "Requêtes enregistrées", - "unifiedSearch.query.queryBar.syntaxOptionsTitle": "Options de syntaxe", - "unifiedSearch.filter.applyFilterActionTitle": "Appliquer le filtre à la vue en cours", - "unifiedSearch.filter.applyFilters.popupHeader": "Sélectionner les filtres à appliquer", - "unifiedSearch.filter.applyFiltersPopup.cancelButtonLabel": "Annuler", - "unifiedSearch.filter.applyFiltersPopup.saveButtonLabel": "Appliquer", - "unifiedSearch.filter.filterBar.addFilterButtonLabel": "Ajouter un filtre", - "unifiedSearch.filter.filterBar.deleteFilterButtonLabel": "Supprimer", - "unifiedSearch.filter.filterBar.disabledFilterPrefix": "Désactivé", - "unifiedSearch.filter.filterBar.disableFilterButtonLabel": "Désactiver temporairement", - "unifiedSearch.filter.filterBar.editFilterButtonLabel": "Modifier le filtre", - "unifiedSearch.filter.filterBar.enableFilterButtonLabel": "Réactiver", - "unifiedSearch.filter.filterBar.excludeFilterButtonLabel": "Exclure les résultats", - "unifiedSearch.filter.filterBar.filterItemBadgeAriaLabel": "Actions de filtrage", - "unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel": "Supprimer {filter}", - "unifiedSearch.filter.filterBar.includeFilterButtonLabel": "Inclure les résultats", - "unifiedSearch.filter.filterBar.indexPatternSelectPlaceholder": "Sélectionner un modèle d'indexation", - "unifiedSearch.filter.filterBar.labelErrorInfo": "Modèle d'indexation {indexPattern} introuvable", - "unifiedSearch.filter.filterBar.labelErrorText": "Erreur", - "unifiedSearch.filter.filterBar.labelWarningInfo": "Le champ {fieldName} n'existe pas dans la vue en cours.", - "unifiedSearch.filter.filterBar.labelWarningText": "Avertissement", - "unifiedSearch.filter.filterBar.moreFilterActionsMessage": "Filtre : {innerText}. Sélectionner pour plus d’actions de filtrage.", - "unifiedSearch.filter.filterBar.negatedFilterPrefix": "NON ", - "unifiedSearch.filter.filterBar.pinFilterButtonLabel": "Épingler dans toutes les applications", - "unifiedSearch.filter.filterBar.pinnedFilterPrefix": "Épinglé", - "unifiedSearch.filter.filterBar.unpinFilterButtonLabel": "Désépingler", - "unifiedSearch.filter.filterEditor.cancelButtonLabel": "Annuler", - "unifiedSearch.filter.filterEditor.createCustomLabelInputLabel": "Étiquette personnalisée", - "unifiedSearch.filter.filterEditor.createCustomLabelSwitchLabel": "Créer une étiquette personnalisée ?", - "unifiedSearch.filter.filterEditor.doesNotExistOperatorOptionLabel": "n'existe pas", - "unifiedSearch.filter.filterEditor.editFilterPopupTitle": "Modifier le filtre", - "unifiedSearch.filter.filterEditor.editFilterValuesButtonLabel": "Modifier les valeurs du filtre", - "unifiedSearch.filter.filterEditor.editQueryDslButtonLabel": "Modifier en tant que Query DSL", - "unifiedSearch.filter.filterEditor.existsOperatorOptionLabel": "existe", - "unifiedSearch.filter.filterEditor.falseOptionLabel": "false", - "unifiedSearch.filter.filterEditor.fieldSelectLabel": "Champ", - "unifiedSearch.filter.filterEditor.fieldSelectPlaceholder": "Sélectionner d'abord un champ", - "unifiedSearch.filter.filterEditor.indexPatternSelectLabel": "Modèle d'indexation", - "unifiedSearch.filter.filterEditor.isBetweenOperatorOptionLabel": "est entre", - "unifiedSearch.filter.filterEditor.isNotBetweenOperatorOptionLabel": "n'est pas entre", - "unifiedSearch.filter.filterEditor.isNotOneOfOperatorOptionLabel": "n'est pas l'une des options suivantes", - "unifiedSearch.filter.filterEditor.isNotOperatorOptionLabel": "n'est pas", - "unifiedSearch.filter.filterEditor.isOneOfOperatorOptionLabel": "est l'une des options suivantes", - "unifiedSearch.filter.filterEditor.isOperatorOptionLabel": "est", - "unifiedSearch.filter.filterEditor.operatorSelectLabel": "Opérateur", - "unifiedSearch.filter.filterEditor.operatorSelectPlaceholderSelect": "Sélectionner", - "unifiedSearch.filter.filterEditor.operatorSelectPlaceholderWaiting": "En attente", - "unifiedSearch.filter.filterEditor.queryDslLabel": "Query DSL d'Elasticsearch", - "unifiedSearch.filter.filterEditor.rangeEndInputPlaceholder": "Fin de la plage", - "unifiedSearch.filter.filterEditor.rangeInputLabel": "Plage", - "unifiedSearch.filter.filterEditor.rangeStartInputPlaceholder": "Début de la plage", - "unifiedSearch.filter.filterEditor.saveButtonLabel": "Enregistrer", - "unifiedSearch.filter.filterEditor.trueOptionLabel": "vrai", - "unifiedSearch.filter.filterEditor.valueInputLabel": "Valeur", - "unifiedSearch.filter.filterEditor.valueInputPlaceholder": "Saisir une valeur", - "unifiedSearch.filter.filterEditor.valueSelectPlaceholder": "Sélectionner une valeur", - "unifiedSearch.filter.filterEditor.valuesSelectLabel": "Valeurs", - "unifiedSearch.filter.filterEditor.valuesSelectPlaceholder": "Sélectionner des valeurs", - "unifiedSearch.filter.options.changeAllFiltersButtonLabel": "Changer tous les filtres", - "unifiedSearch.filter.options.deleteAllFiltersButtonLabel": "Tout supprimer", - "unifiedSearch.filter.options.disableAllFiltersButtonLabel": "Tout désactiver", - "unifiedSearch.filter.options.enableAllFiltersButtonLabel": "Tout activer", - "unifiedSearch.filter.options.invertDisabledFiltersButtonLabel": "Inverser l’activation/désactivation", - "unifiedSearch.filter.options.invertNegatedFiltersButtonLabel": "Inverser l'inclusion", - "unifiedSearch.filter.options.pinAllFiltersButtonLabel": "Tout épingler", - "unifiedSearch.filter.options.unpinAllFiltersButtonLabel": "Tout désépingler", - "unifiedSearch.filter.searchBar.changeAllFiltersTitle": "Changer tous les filtres", "devTools.badge.readOnly.text": "Lecture seule", "devTools.badge.readOnly.tooltip": "Enregistrement impossible", "devTools.breadcrumb.homeLabel": "Outils de développement", @@ -2991,7 +2873,6 @@ "discover.field.mappingConflict": "Ce champ est défini avec plusieurs types (chaîne, entier, etc.) dans les différents index qui correspondent à ce modèle. Vous pouvez toujours utiliser ce champ conflictuel, mais il sera indisponible pour les fonctions qui nécessitent que Kibana en connaisse le type. Pour corriger ce problème, vous devrez réindexer vos données.", "discover.field.mappingConflict.title": "Conflit de mapping", "discover.field.title": "{fieldName} ({fieldDisplayName})", - "discover.fieldChooser.dataViews.createNewDataView": "Créer une nouvelle vue de données", "discover.fieldChooser.detailViews.emptyStringText": "Chaîne vide", "discover.fieldChooser.detailViews.existsInRecordsText": "Existe dans {value} / {totalValue} enregistrements", "discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "Exclure le {field} : \"{value}\"", @@ -3028,10 +2909,6 @@ "discover.fieldChooser.filter.toggleButton.no": "non", "discover.fieldChooser.filter.toggleButton.yes": "oui", "discover.fieldChooser.filter.typeLabel": "Type", - "discover.fieldChooser.indexPattern.changeDataViewTitle": "Changer de vue de données", - "discover.fieldChooser.indexPatterns.actionsPopoverLabel": "Paramètres Vue de données", - "discover.fieldChooser.indexPatterns.addFieldButton": "Ajouter un champ", - "discover.fieldChooser.indexPatterns.manageFieldButton": "Gérer les paramètres", "discover.fieldChooser.searchPlaceHolder": "Rechercher les noms de champs", "discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel": "Masquer les paramètres de filtre de champs", "discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel": "Afficher les paramètres de filtre de champs", @@ -8019,7 +7896,6 @@ "xpack.apm.settings.customLink.delete": "Supprimer", "xpack.apm.settings.customLink.delete.failed": "Impossible de supprimer le lien personnalisé", "xpack.apm.settings.customLink.delete.successed": "Lien personnalisé supprimé.", - "xpack.apm.settings.customLink.emptyPromptText": "Nous allons y remédier ! Vous pouvez ajouter des liens personnalisés au menu contextuel Actions à partir des détails de transaction de chaque service. Créez un lien utile vers le portail d'assistance de votre société, ou ouvrez un nouveau rapport de bug. Pour en savoir plus, consultez notre documentation.", "xpack.apm.settings.customLink.emptyPromptTitle": "Aucun lien trouvé.", "xpack.apm.settings.customLink.flyout.action.title": "Lien", "xpack.apm.settings.customLink.flyout.close": "Fermer", @@ -8359,7 +8235,6 @@ "xpack.apm.views.metrics.title": "Indicateurs", "xpack.apm.views.nodes.title": "JVM", "xpack.apm.views.overview.title": "Aperçu", - "xpack.apm.views.serviceGroups.breadcrumbLabel": "Services", "xpack.apm.views.serviceGroups.title": "Groupes de services", "xpack.apm.views.serviceInventory.title": "Services", "xpack.apm.views.serviceMap.title": "Carte des services", @@ -21840,13 +21715,10 @@ "xpack.observability.rules.rulesTable.columns.statusTitle": "Statut", "xpack.observability.rules.rulesTable.pluralTitle": "règles", "xpack.observability.rules.rulesTable.ruleStatusActive": "Actif", - "xpack.observability.rules.rulesTable.ruleStatusDisabled": "Désactivé", - "xpack.observability.rules.rulesTable.ruleStatusEnabled": "Activé", "xpack.observability.rules.rulesTable.ruleStatusError": "Erreur", "xpack.observability.rules.rulesTable.ruleStatusLicenseError": "Erreur de licence", "xpack.observability.rules.rulesTable.ruleStatusOk": "Ok", "xpack.observability.rules.rulesTable.ruleStatusPending": "En attente", - "xpack.observability.rules.rulesTable.ruleStatusSnoozedIndefinitely": "Répété indéfiniment", "xpack.observability.rules.rulesTable.ruleStatusUnknown": "Inconnu", "xpack.observability.rules.rulesTable.ruleStatusWarning": "avertissement", "xpack.observability.rules.rulesTable.singleTitle": "règle", @@ -23894,13 +23766,10 @@ "xpack.securitySolution.console.builtInCommands.clearAbout": "Purger la mémoire tampon de la console", "xpack.securitySolution.console.builtInCommands.helpAbout": "Afficher la liste des commandes disponibles", "xpack.securitySolution.console.commandList.footerText": "Pour plus d’informations sur les commandes ci-dessus, utilisez l’argument {helpOption}. Exemple : {cmdExample}", - "xpack.securitySolution.console.commandOutput.runInBackgroundButtonLabel": "Exécuter en arrière-plan", - "xpack.securitySolution.console.commandOutput.runInBackgroundMsg": "La réponse à la commande prend un peu de temps. Cliquez ici pour l’exécuter en arrière-plan et être averti lors de la réception de la réponse.", "xpack.securitySolution.console.commandUsage.atLeastOneOptionRequiredMessage": "Remarque : au moins une option doit être utilisée.", "xpack.securitySolution.console.commandUsage.inputUsage": "Utilisation :", "xpack.securitySolution.console.commandUsage.optionsLabel": "Options :", "xpack.securitySolution.console.commandValidation.argSupportedOnlyOnce": "cet argument ne peut être utilisé qu’une fois : {argName}.", - "xpack.securitySolution.console.commandValidation.cmdHelpTitle": "commande {cmdName}", "xpack.securitySolution.console.commandValidation.invalidArgValue": "valeur d’argument non valide : {argName}. {error}", "xpack.securitySolution.console.commandValidation.missingRequiredArg": "argument requis manquant : {argName}", "xpack.securitySolution.console.commandValidation.mustHaveArgs": "arguments requis manquants : {requiredArgs}", @@ -25965,7 +25834,6 @@ "xpack.securitySolution.hosts.hostScoreOverTime.riskScore": "Score de risque", "xpack.securitySolution.hosts.hostScoreOverTime.riskyLabel": "À risque", "xpack.securitySolution.hosts.hostScoreOverTime.riskyThresholdHeader": "Seuil du niveau À risque", - "xpack.securitySolution.hosts.hostScoreOverTime.title": "Score de risque de l'hôte sur la durée", "xpack.securitySolution.hosts.kqlPlaceholder": "par ex. hôte.nom : \"foo\"", "xpack.securitySolution.hosts.navigation.alertsTitle": "Alertes externes", "xpack.securitySolution.hosts.navigation.allHostsTitle": "Tous les hôtes", @@ -31100,48 +30968,28 @@ "unifiedSearch.noDataPopover.title": "Ensemble de données vide", "unifiedSearch.query.queryBar.clearInputLabel": "Effacer l'entrée", "unifiedSearch.query.queryBar.comboboxAriaLabel": "Rechercher et filtrer la page {pageType}", - "unifiedSearch.query.queryBar.kqlFullLanguageName": "Langage de requête Kibana", "unifiedSearch.query.queryBar.kqlLanguageName": "KQL", "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoDocLinkText": "documents", "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoOptOutText": "Ne plus afficher", "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoText": "Il semblerait que votre requête porte sur un champ imbriqué. Selon le résultat visé, il existe plusieurs façons de construire une syntaxe KQL pour des requêtes imbriquées. Apprenez-en plus avec notre {link}.", "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoTitle": "Syntaxe de requête imbriquée KQL", - "unifiedSearch.query.queryBar.kqlOffLabel": "Désactivé", - "unifiedSearch.query.queryBar.kqlOnLabel": "Activé", - "unifiedSearch.query.queryBar.languageSwitcher.toText": "Passer au langage de requête Kibana pour la recherche", "unifiedSearch.query.queryBar.luceneLanguageName": "Lucene", "unifiedSearch.query.queryBar.searchInputAriaLabel": "Commencer à taper pour rechercher et filtrer la page {pageType}", - "unifiedSearch.query.queryBar.searchInputPlaceholder": "Recherche", - "unifiedSearch.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) offre une syntaxe de requête simplifiée et la prise en charge des champs scriptés. KQL offre également une fonctionnalité de saisie semi-automatique. Si vous désactivez KQL, {nonKqlModeHelpText}.", - "unifiedSearch.query.queryBar.syntaxOptionsDescription.nonKqlModeHelpText": "Kibana utilise Lucene.", - "unifiedSearch.search.searchBar.savedQueryDescriptionLabelText": "Description", "unifiedSearch.search.searchBar.savedQueryDescriptionText": "Enregistrez le texte et les filtres de la requête que vous souhaitez réutiliser.", "unifiedSearch.search.searchBar.savedQueryForm.titleConflictText": "Ce nom est en conflit avec une requête enregistrée existante.", - "unifiedSearch.search.searchBar.savedQueryFormCancelButtonText": "Annuler", "unifiedSearch.search.searchBar.savedQueryFormSaveButtonText": "Enregistrer", - "unifiedSearch.search.searchBar.savedQueryFormTitle": "Enregistrer la requête", "unifiedSearch.search.searchBar.savedQueryIncludeFiltersLabelText": "Inclure les filtres", "unifiedSearch.search.searchBar.savedQueryIncludeTimeFilterLabelText": "Inclure le filtre temporel", "unifiedSearch.search.searchBar.savedQueryNameHelpText": "Un nom est requis. Le nom ne peut pas contenir d'espace vide au début ou à la fin. Le nom doit être unique.", "unifiedSearch.search.searchBar.savedQueryNameLabelText": "Nom", "unifiedSearch.search.searchBar.savedQueryNoSavedQueriesText": "Aucune requête enregistrée.", - "unifiedSearch.search.searchBar.savedQueryPopoverButtonText": "Voir les requêtes enregistrées", - "unifiedSearch.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "Effacer la requête enregistrée en cours", - "unifiedSearch.search.searchBar.savedQueryPopoverClearButtonText": "Effacer", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "Annuler", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "Supprimer", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "Supprimer \"{savedQueryName}\" ?", - "unifiedSearch.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "Supprimer la requête enregistrée {savedQueryName}", "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "Enregistrer en tant que nouvelle requête enregistrée", "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "Enregistrer en tant que nouvelle", - "unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "Enregistrer une nouvelle requête enregistrée", - "unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonText": "Enregistrer la requête en cours", "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "Enregistrer les modifications apportées à {title}", "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText": "Enregistrer les modifications", - "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "Bouton de requête enregistrée {savedQueryName}", - "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "Description de {savedQueryName}", - "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "Bouton de requête enregistrée {savedQueryName} sélectionné. Appuyez pour effacer les modifications.", - "unifiedSearch.search.searchBar.savedQueryPopoverTitleText": "Requêtes enregistrées", "unifiedSearch.search.unableToGetSavedQueryToastTitle": "Impossible de charger la requête enregistrée {savedQueryId}", "unifiedSearch.query.queryBar.syntaxOptionsTitle": "Options de syntaxe", "unifiedSearch.filter.applyFilterActionTitle": "Appliquer le filtre à la vue en cours", @@ -31193,22 +31041,17 @@ "unifiedSearch.filter.filterEditor.rangeEndInputPlaceholder": "Fin de la plage", "unifiedSearch.filter.filterEditor.rangeInputLabel": "Plage", "unifiedSearch.filter.filterEditor.rangeStartInputPlaceholder": "Début de la plage", - "unifiedSearch.filter.filterEditor.saveButtonLabel": "Enregistrer", "unifiedSearch.filter.filterEditor.trueOptionLabel": "vrai", "unifiedSearch.filter.filterEditor.valueInputLabel": "Valeur", "unifiedSearch.filter.filterEditor.valueInputPlaceholder": "Saisir une valeur", "unifiedSearch.filter.filterEditor.valueSelectPlaceholder": "Sélectionner une valeur", "unifiedSearch.filter.filterEditor.valuesSelectLabel": "Valeurs", "unifiedSearch.filter.filterEditor.valuesSelectPlaceholder": "Sélectionner des valeurs", - "unifiedSearch.filter.options.changeAllFiltersButtonLabel": "Changer tous les filtres", - "unifiedSearch.filter.options.deleteAllFiltersButtonLabel": "Tout supprimer", "unifiedSearch.filter.options.disableAllFiltersButtonLabel": "Tout désactiver", "unifiedSearch.filter.options.enableAllFiltersButtonLabel": "Tout activer", - "unifiedSearch.filter.options.invertDisabledFiltersButtonLabel": "Inverser l’activation/désactivation", "unifiedSearch.filter.options.invertNegatedFiltersButtonLabel": "Inverser l'inclusion", "unifiedSearch.filter.options.pinAllFiltersButtonLabel": "Tout épingler", "unifiedSearch.filter.options.unpinAllFiltersButtonLabel": "Tout désépingler", - "unifiedSearch.filter.searchBar.changeAllFiltersTitle": "Changer tous les filtres", "unifiedSearch.kueryAutocomplete.andOperatorDescription": "Nécessite que {bothArguments} soient ''vrai''.", "unifiedSearch.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "les deux arguments", "unifiedSearch.kueryAutocomplete.equalOperatorDescription": "{equals} une certaine valeur", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4b8f0b344b2b5..9b42d92c275e9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -701,13 +701,10 @@ "xpack.lens.indexPattern.timeShiftSmallWarning": "{label}は{columnTimeShift}の時間シフトを使用しています。これは{interval}の日付ヒストグラム間隔よりも小さいです。不一致のデータを防止するには、時間シフトとして{interval}を使用します。", "xpack.lens.indexPattern.uniqueLabel": "{label} [{num}]", "xpack.lens.indexPattern.useAsTopLevelAgg": "最初にこのフィールドでグループ化", - "xpack.lens.indexPatterns.actionsPopoverLabel": "データビュー設定", - "xpack.lens.indexPatterns.addFieldButton": "フィールドをデータビューに追加", "xpack.lens.indexPatterns.clearFiltersLabel": "名前とタイプフィルターを消去", "xpack.lens.indexPatterns.fieldFiltersLabel": "タイプでフィルタリング", "xpack.lens.indexPatterns.fieldSearchLiveRegion": "{availableFields}使用可能な{availableFields, plural, other {フィールド}}。{emptyFields}空の{emptyFields, plural, other {フィールド}}。 {metaFields}メタ{metaFields, plural, other {フィールド}}。", "xpack.lens.indexPatterns.filterByNameLabel": "検索フィールド名", - "xpack.lens.indexPatterns.manageFieldButton": "データビューフィールドを管理", "xpack.lens.indexPatterns.noAvailableDataLabel": "データを含むフィールドはありません。", "xpack.lens.indexPatterns.noDataLabel": "フィールドがありません。", "xpack.lens.indexPatterns.noEmptyDataLabel": "空のフィールドがありません。", @@ -2702,10 +2699,6 @@ "data.searchSessions.sessionService.sessionObjectFetchError": "検索セッション情報を取得できませんでした", "data.triggers.applyFilterDescription": "Kibanaフィルターが適用されるとき。単一の値または範囲フィルターにすることができます。", "data.triggers.applyFilterTitle": "フィルターを適用", - "dataViews.deprecations.scriptedFields.manualStepOneMessage": "[スタック管理]>[Kibana]>[データビュー]に移動します。", - "dataViews.deprecations.scriptedFields.manualStepTwoMessage": "ランタイムフィールドを使用するには、スクリプト化されたフィールドがある{numberOfIndexPatternsWithScriptedFields}データビューを更新します。ほとんどの場合、既存のスクリプトを移行するには、「return ;」から「emit();」に変更する必要があります。1つ以上のスクリプト化されたフィールドがあるデータビュー:{allTitles}", - "dataViews.deprecations.scriptedFieldsMessage": "スクリプト化されたフィールドを使用する{numberOfIndexPatternsWithScriptedFields}データビュー({titlesPreview}...)があります。スクリプト化されたフィールドは廃止予定であり、今後は削除されます。ランタイムフィールドを使用してください。", - "dataViews.deprecations.scriptedFieldsTitle": "スクリプト化されたフィールドを使用しているデータビューが見つかりました。", "data.mgmt.searchSessions.actionDelete": "削除", "data.mgmt.searchSessions.actionExtend": "延長", "data.mgmt.searchSessions.actionRename": "名前を編集", @@ -2977,7 +2970,6 @@ "discover.field.mappingConflict": "このフィールドは、このパターンと一致するインデックス全体に対して複数の型(文字列、整数など)として定義されています。この競合フィールドを使用することはできますが、Kibana で型を認識する必要がある関数では使用できません。この問題を修正するにはデータのレンダリングが必要です。", "discover.field.mappingConflict.title": "マッピングの矛盾", "discover.field.title": "{fieldName} ({fieldDisplayName})", - "discover.fieldChooser.dataViews.createNewDataView": "新しいデータビューを作成", "discover.fieldChooser.detailViews.emptyStringText": "空の文字列", "discover.fieldChooser.detailViews.existsInRecordsText": "{value} / {totalValue}件のレコードに存在", "discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "{field}を除外:\"{value}\"", @@ -3014,10 +3006,6 @@ "discover.fieldChooser.filter.toggleButton.no": "いいえ", "discover.fieldChooser.filter.toggleButton.yes": "はい", "discover.fieldChooser.filter.typeLabel": "型", - "discover.fieldChooser.indexPattern.changeDataViewTitle": "データビューを変更", - "discover.fieldChooser.indexPatterns.actionsPopoverLabel": "データビュー設定", - "discover.fieldChooser.indexPatterns.addFieldButton": "フィールドの追加", - "discover.fieldChooser.indexPatterns.manageFieldButton": "設定の管理", "discover.fieldChooser.searchPlaceHolder": "検索フィールド名", "discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel": "フィールド設定を非表示", "discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel": "フィールド設定を表示", @@ -8005,7 +7993,6 @@ "xpack.apm.settings.customLink.delete": "削除", "xpack.apm.settings.customLink.delete.failed": "カスタムリンクを削除できませんでした", "xpack.apm.settings.customLink.delete.successed": "カスタムリンクを削除しました。", - "xpack.apm.settings.customLink.emptyPromptText": "変更しましょう。サービスごとのトランザクションの詳細でアクションコンテキストメニューにカスタムリンクを追加できます。自社のサポートポータルへの役立つリンクを作成するか、新しい不具合レポートを発行します。詳細はドキュメントをご覧ください。", "xpack.apm.settings.customLink.emptyPromptTitle": "リンクが見つかりません。", "xpack.apm.settings.customLink.flyout.action.title": "リンク", "xpack.apm.settings.customLink.flyout.close": "閉じる", @@ -8344,7 +8331,6 @@ "xpack.apm.views.metrics.title": "メトリック", "xpack.apm.views.nodes.title": "JVM", "xpack.apm.views.overview.title": "概要", - "xpack.apm.views.serviceGroups.breadcrumbLabel": "サービス", "xpack.apm.views.serviceGroups.title": "サービスグループ", "xpack.apm.views.serviceInventory.title": "サービス", "xpack.apm.views.serviceMap.title": "サービスマップ", @@ -21882,13 +21868,10 @@ "xpack.observability.rules.rulesTable.columns.statusTitle": "ステータス", "xpack.observability.rules.rulesTable.pluralTitle": "ルール", "xpack.observability.rules.rulesTable.ruleStatusActive": "アクティブ", - "xpack.observability.rules.rulesTable.ruleStatusDisabled": "無効", - "xpack.observability.rules.rulesTable.ruleStatusEnabled": "有効", "xpack.observability.rules.rulesTable.ruleStatusError": "エラー", "xpack.observability.rules.rulesTable.ruleStatusLicenseError": "ライセンスエラー", "xpack.observability.rules.rulesTable.ruleStatusOk": "OK", "xpack.observability.rules.rulesTable.ruleStatusPending": "保留中", - "xpack.observability.rules.rulesTable.ruleStatusSnoozedIndefinitely": "無期限にスヌーズ", "xpack.observability.rules.rulesTable.ruleStatusUnknown": "不明", "xpack.observability.rules.rulesTable.ruleStatusWarning": "警告", "xpack.observability.rules.rulesTable.singleTitle": "ルール", @@ -23934,13 +23917,10 @@ "xpack.securitySolution.console.builtInCommands.clearAbout": "コンソールバッファーを消去", "xpack.securitySolution.console.builtInCommands.helpAbout": "使用可能なコマンドのリストを表示", "xpack.securitySolution.console.commandList.footerText": "上記のコマンドの詳細については、{helpOption}引数を使用してください。例:{cmdExample}", - "xpack.securitySolution.console.commandOutput.runInBackgroundButtonLabel": "バックグラウンドで実行", - "xpack.securitySolution.console.commandOutput.runInBackgroundMsg": "コマンド応答には時間がかかります。ここをクリックするとバックグラウンドで実行し、応答を受信したときに通知が表示されます", "xpack.securitySolution.console.commandUsage.atLeastOneOptionRequiredMessage": "注記:1つ以上のオプションを使用する必要があります", "xpack.securitySolution.console.commandUsage.inputUsage": "使用方法:", "xpack.securitySolution.console.commandUsage.optionsLabel": "オプション:", "xpack.securitySolution.console.commandValidation.argSupportedOnlyOnce": "引数{argName}は一度だけ使用できます", - "xpack.securitySolution.console.commandValidation.cmdHelpTitle": "{cmdName}コマンド", "xpack.securitySolution.console.commandValidation.invalidArgValue": "無効な引数値:{argName}。{error}", "xpack.securitySolution.console.commandValidation.missingRequiredArg": "不足している必須の引数:{argName}", "xpack.securitySolution.console.commandValidation.mustHaveArgs": "不足している必須の引数:{requiredArgs}", @@ -26018,7 +25998,6 @@ "xpack.securitySolution.hosts.hostScoreOverTime.riskScore": "リスクスコア", "xpack.securitySolution.hosts.hostScoreOverTime.riskyLabel": "高リスク", "xpack.securitySolution.hosts.hostScoreOverTime.riskyThresholdHeader": "高リスクしきい値", - "xpack.securitySolution.hosts.hostScoreOverTime.title": "経時的なホストリスクスコア", "xpack.securitySolution.hosts.kqlPlaceholder": "例:host.name:\"foo\"", "xpack.securitySolution.hosts.navigation.alertsTitle": "外部アラート", "xpack.securitySolution.hosts.navigation.allHostsTitle": "すべてのホスト", @@ -31194,55 +31173,34 @@ "xpack.watcher.watchActions.webhook.portIsRequiredValidationMessage": "Webフックポートが必要です。", "xpack.watcher.watchActions.webhook.usernameIsRequiredIfPasswordValidationMessage": "ユーザー名が必要です。", "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", - "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。", - "unifiedSearch.search.searchBar.savedQueryDescriptionLabelText": "説明", "unifiedSearch.search.searchBar.savedQueryDescriptionText": "再度使用するクエリテキストとフィルターを保存します。", "unifiedSearch.search.searchBar.savedQueryForm.titleConflictText": "タイトルがすでに保存されているクエリに使用されています", - "unifiedSearch.search.searchBar.savedQueryFormCancelButtonText": "キャンセル", "unifiedSearch.search.searchBar.savedQueryFormSaveButtonText": "保存", - "unifiedSearch.search.searchBar.savedQueryFormTitle": "クエリを保存", "unifiedSearch.search.searchBar.savedQueryIncludeFiltersLabelText": "フィルターを含める", "unifiedSearch.search.searchBar.savedQueryIncludeTimeFilterLabelText": "時間フィルターを含める", "unifiedSearch.search.searchBar.savedQueryNameHelpText": "名前は必須です。名前の始めと終わりにはスペースを使用できません。名前は一意でなければなりません。", "unifiedSearch.search.searchBar.savedQueryNameLabelText": "名前", "unifiedSearch.search.searchBar.savedQueryNoSavedQueriesText": "保存されたクエリがありません。", - "unifiedSearch.search.searchBar.savedQueryPopoverButtonText": "保存されたクエリを表示", - "unifiedSearch.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "現在保存されているクエリを消去", - "unifiedSearch.search.searchBar.savedQueryPopoverClearButtonText": "クリア", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "キャンセル", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "削除", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "「{savedQueryName}」を削除しますか?", - "unifiedSearch.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "保存されたクエリ {savedQueryName} を削除", "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "新規保存クエリを保存", "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "新規保存", - "unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "新規保存クエリを保存", - "unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonText": "現在のクエリを保存", "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "{title} への変更を保存", "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText": "変更を保存", - "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "保存クエリボタン {savedQueryName}", - "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName}の説明", - "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "選択されたクエリボタン {savedQueryName} を保存しました。変更を破棄するには押してください。", - "unifiedSearch.search.searchBar.savedQueryPopoverTitleText": "保存されたクエリ", "unifiedSearch.noDataPopover.content": "この時間範囲にはデータが含まれていません。表示するフィールドを増やし、グラフを作成するには、時間範囲を広げるか、調整してください。", "unifiedSearch.noDataPopover.dismissAction": "今後表示しない", "unifiedSearch.noDataPopover.subtitle": "ヒント", "unifiedSearch.noDataPopover.title": "空のデータセット", "unifiedSearch.query.queryBar.clearInputLabel": "インプットを消去", "unifiedSearch.query.queryBar.comboboxAriaLabel": "{pageType} ページの検索とフィルタリング", - "unifiedSearch.query.queryBar.kqlFullLanguageName": "Kibana クエリ言語", "unifiedSearch.query.queryBar.kqlLanguageName": "KQL", "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoDocLinkText": "ドキュメント", "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoOptOutText": "今後表示しない", "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoText": "ネストされたフィールドをクエリされているようです。ネストされたクエリに対しては、ご希望の結果により異なる方法で KQL 構文を構築することができます。詳細については、{link}をご覧ください。", "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoTitle": "KQL ネストされたクエリ構文", - "unifiedSearch.query.queryBar.kqlOffLabel": "オフ", - "unifiedSearch.query.queryBar.kqlOnLabel": "オン", - "unifiedSearch.query.queryBar.languageSwitcher.toText": "検索用にKibana Query Languageに切り替える", "unifiedSearch.query.queryBar.luceneLanguageName": "Lucene", "unifiedSearch.query.queryBar.searchInputAriaLabel": "{pageType} ページの検索とフィルタリングを行うには入力を開始してください", - "unifiedSearch.query.queryBar.searchInputPlaceholder": "検索", - "unifiedSearch.query.queryBar.syntaxOptionsDescription": "{docsLink}(KQL)は、シンプルなクエリ構文とスクリプトフィールドのサポートを提供します。KQLにはオートコンプリート機能もあります。KQLをオフにする場合は、{nonKqlModeHelpText}", - "unifiedSearch.query.queryBar.syntaxOptionsDescription.nonKqlModeHelpText": "KibanaはLuceneを使用します。", "unifiedSearch.search.unableToGetSavedQueryToastTitle": "保存したクエリ {savedQueryId} を読み込めません", "unifiedSearch.query.queryBar.syntaxOptionsTitle": "構文オプション", "unifiedSearch.filter.applyFilterActionTitle": "現在のビューにフィルターを適用", @@ -31295,22 +31253,18 @@ "unifiedSearch.filter.filterEditor.rangeEndInputPlaceholder": "範囲の終了値", "unifiedSearch.filter.filterEditor.rangeInputLabel": "範囲", "unifiedSearch.filter.filterEditor.rangeStartInputPlaceholder": "範囲の開始値", - "unifiedSearch.filter.filterEditor.saveButtonLabel": "保存", "unifiedSearch.filter.filterEditor.trueOptionLabel": "true", "unifiedSearch.filter.filterEditor.valueInputLabel": "値", "unifiedSearch.filter.filterEditor.valueInputPlaceholder": "値を入力", "unifiedSearch.filter.filterEditor.valueSelectPlaceholder": "値を選択", "unifiedSearch.filter.filterEditor.valuesSelectLabel": "値", "unifiedSearch.filter.filterEditor.valuesSelectPlaceholder": "値を選択", - "unifiedSearch.filter.options.changeAllFiltersButtonLabel": "すべてのフィルターの変更", - "unifiedSearch.filter.options.deleteAllFiltersButtonLabel": "すべて削除", "unifiedSearch.filter.options.disableAllFiltersButtonLabel": "すべて無効にする", "unifiedSearch.filter.options.enableAllFiltersButtonLabel": "すべて有効にする", - "unifiedSearch.filter.options.invertDisabledFiltersButtonLabel": "有効・無効を反転", "unifiedSearch.filter.options.invertNegatedFiltersButtonLabel": "含める・除外を反転", "unifiedSearch.filter.options.pinAllFiltersButtonLabel": "すべてピン付け", "unifiedSearch.filter.options.unpinAllFiltersButtonLabel": "すべてのピンを外す", - "unifiedSearch.filter.searchBar.changeAllFiltersTitle": "すべてのフィルターの変更", + "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。", "unifiedSearch.kueryAutocomplete.andOperatorDescription": "{bothArguments} が true であることを条件とする", "unifiedSearch.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "両方の引数", "unifiedSearch.kueryAutocomplete.equalOperatorDescription": "一部の値に{equals}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 649d6e648c0ec..dd87bbaa23fef 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -706,13 +706,10 @@ "xpack.lens.indexPattern.timeShiftSmallWarning": "{label} 使用的时间偏移 {columnTimeShift} 小于 Date Histogram 时间间隔 {interval} 。要防止数据不匹配,请使用 {interval} 的倍数作为时间偏移。", "xpack.lens.indexPattern.uniqueLabel": "{label} [{num}]", "xpack.lens.indexPattern.useAsTopLevelAgg": "先按此字段分组", - "xpack.lens.indexPatterns.actionsPopoverLabel": "数据视图设置", - "xpack.lens.indexPatterns.addFieldButton": "将字段添加到数据视图", "xpack.lens.indexPatterns.clearFiltersLabel": "清除名称和类型筛选", "xpack.lens.indexPatterns.fieldFiltersLabel": "按类型筛选", "xpack.lens.indexPatterns.fieldSearchLiveRegion": "{availableFields} 个可用{availableFields, plural, other {字段}}。{emptyFields} 个空{emptyFields, plural, other {字段}}。{metaFields} 个元{metaFields, plural,other {字段}}。", "xpack.lens.indexPatterns.filterByNameLabel": "搜索字段名称", - "xpack.lens.indexPatterns.manageFieldButton": "管理数据视图字段", "xpack.lens.indexPatterns.noAvailableDataLabel": "没有包含数据的可用字段。", "xpack.lens.indexPatterns.noDataLabel": "无字段。", "xpack.lens.indexPatterns.noEmptyDataLabel": "无空字段。", @@ -2810,10 +2807,6 @@ "data.searchSessionName.saveButtonText": "保存", "data.sessions.management.flyoutText": "此搜索会话的配置", "data.sessions.management.flyoutTitle": "检查搜索会话", - "dataViews.deprecations.scriptedFields.manualStepOneMessage": "导航到“堆栈管理”>“Kibana”>“索引模式”。", - "dataViews.deprecations.scriptedFields.manualStepTwoMessage": "更新 {numberOfIndexPatternsWithScriptedFields} 个具有脚本字段的索引模式以改为使用运行时字段。多数情况下,要迁移现有脚本,您需要将“return ;”更改为“emit();”。至少有一个脚本字段的索引模式:{allTitles}", - "dataViews.deprecations.scriptedFieldsMessage": "您具有 {numberOfIndexPatternsWithScriptedFields} 个使用脚本字段的索引模式 ({titlesPreview}...)。脚本字段已过时,将在未来移除。请改为使用运行时脚本。", - "dataViews.deprecations.scriptedFieldsTitle": "找到使用脚本字段的索引模式", "dataViews.deprecations.scriptedFields.manualStepOneMessage": "导航到“堆栈管理”>“Kibana”>“数据视图”。", "dataViews.deprecations.scriptedFields.manualStepTwoMessage": "更新 {numberOfIndexPatternsWithScriptedFields} 个具有脚本字段的数据视图以改为使用运行时字段。多数情况下,要迁移现有脚本,您需要将“return ;”更改为“emit();”。至少有一个脚本字段的数据视图:{allTitles}", "dataViews.deprecations.scriptedFieldsMessage": "您具有 {numberOfIndexPatternsWithScriptedFields} 个使用脚本字段的数据视图 ({titlesPreview}...)。脚本字段已过时,将在未来移除。请改为使用运行时脚本。", @@ -2988,7 +2981,6 @@ "discover.field.mappingConflict": "此字段在匹配此模式的各个索引中已定义为若干类型(字符串、整数等)。您可能仍可以使用此冲突字段,但它无法用于需要 Kibana 知道其类型的函数。要解决此问题,需要重新索引您的数据。", "discover.field.mappingConflict.title": "映射冲突", "discover.field.title": "{fieldName} ({fieldDisplayName})", - "discover.fieldChooser.dataViews.createNewDataView": "创建新的数据视图", "discover.fieldChooser.detailViews.emptyStringText": "空字符串", "discover.fieldChooser.detailViews.existsInRecordsText": "存在于 {value} / {totalValue} 条记录中", "discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "筛除 {field}:“{value}”", @@ -3025,10 +3017,6 @@ "discover.fieldChooser.filter.toggleButton.no": "否", "discover.fieldChooser.filter.toggleButton.yes": "是", "discover.fieldChooser.filter.typeLabel": "类型", - "discover.fieldChooser.indexPattern.changeDataViewTitle": "更改数据视图", - "discover.fieldChooser.indexPatterns.actionsPopoverLabel": "数据视图设置", - "discover.fieldChooser.indexPatterns.addFieldButton": "添加字段", - "discover.fieldChooser.indexPatterns.manageFieldButton": "管理设置", "discover.fieldChooser.searchPlaceHolder": "搜索字段名称", "discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel": "隐藏字段筛选设置", "discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel": "显示字段筛选设置", @@ -8025,7 +8013,6 @@ "xpack.apm.settings.customLink.delete": "删除", "xpack.apm.settings.customLink.delete.failed": "无法删除定制链接", "xpack.apm.settings.customLink.delete.successed": "已删除定制链接。", - "xpack.apm.settings.customLink.emptyPromptText": "让我们改动一下!可以通过每个服务的事务详情将定制链接添加到“操作”上下文菜单。创建指向公司支持门户或用于提交新错误报告的有用链接。在我们的文档中详细了解。", "xpack.apm.settings.customLink.emptyPromptTitle": "未找到链接。", "xpack.apm.settings.customLink.flyout.action.title": "链接", "xpack.apm.settings.customLink.flyout.close": "关闭", @@ -8365,7 +8352,6 @@ "xpack.apm.views.metrics.title": "指标", "xpack.apm.views.nodes.title": "JVM", "xpack.apm.views.overview.title": "概览", - "xpack.apm.views.serviceGroups.breadcrumbLabel": "服务", "xpack.apm.views.serviceGroups.title": "服务组", "xpack.apm.views.serviceInventory.title": "服务", "xpack.apm.views.serviceMap.title": "服务地图", @@ -21914,13 +21900,10 @@ "xpack.observability.rules.rulesTable.columns.statusTitle": "状态", "xpack.observability.rules.rulesTable.pluralTitle": "规则", "xpack.observability.rules.rulesTable.ruleStatusActive": "活动", - "xpack.observability.rules.rulesTable.ruleStatusDisabled": "已禁用", - "xpack.observability.rules.rulesTable.ruleStatusEnabled": "已启用", "xpack.observability.rules.rulesTable.ruleStatusError": "错误", "xpack.observability.rules.rulesTable.ruleStatusLicenseError": "许可证错误", "xpack.observability.rules.rulesTable.ruleStatusOk": "确定", "xpack.observability.rules.rulesTable.ruleStatusPending": "待处理", - "xpack.observability.rules.rulesTable.ruleStatusSnoozedIndefinitely": "已无限期暂停", "xpack.observability.rules.rulesTable.ruleStatusUnknown": "未知", "xpack.observability.rules.rulesTable.ruleStatusWarning": "警告", "xpack.observability.rules.rulesTable.singleTitle": "规则", @@ -23967,13 +23950,10 @@ "xpack.securitySolution.console.builtInCommands.clearAbout": "清除控制台缓冲区", "xpack.securitySolution.console.builtInCommands.helpAbout": "查看可用命令列表", "xpack.securitySolution.console.commandList.footerText": "有关上述命令的更多详情,请使用 {helpOption} 参数。示例:{cmdExample}", - "xpack.securitySolution.console.commandOutput.runInBackgroundButtonLabel": "在后台运行", - "xpack.securitySolution.console.commandOutput.runInBackgroundMsg": "命令响应花费的时间略长。单击此处以在后台运行,并在收到响应时发送通知", "xpack.securitySolution.console.commandUsage.atLeastOneOptionRequiredMessage": "注意:必须至少使用一个选项", "xpack.securitySolution.console.commandUsage.inputUsage": "用法:", "xpack.securitySolution.console.commandUsage.optionsLabel": "选项:", "xpack.securitySolution.console.commandValidation.argSupportedOnlyOnce": "参数只能使用一次:{argName}", - "xpack.securitySolution.console.commandValidation.cmdHelpTitle": "{cmdName} 命令", "xpack.securitySolution.console.commandValidation.invalidArgValue": "无效的参数值:{argName}。{error}", "xpack.securitySolution.console.commandValidation.missingRequiredArg": "缺少所需参数:{argName}", "xpack.securitySolution.console.commandValidation.mustHaveArgs": "缺少所需参数:{requiredArgs}", @@ -26053,7 +26033,6 @@ "xpack.securitySolution.hosts.hostScoreOverTime.riskScore": "风险分数", "xpack.securitySolution.hosts.hostScoreOverTime.riskyLabel": "有风险", "xpack.securitySolution.hosts.hostScoreOverTime.riskyThresholdHeader": "有风险的阈值", - "xpack.securitySolution.hosts.hostScoreOverTime.title": "一段时间的主机风险分数", "xpack.securitySolution.hosts.kqlPlaceholder": "例如 host.name:“foo”", "xpack.securitySolution.hosts.navigation.alertsTitle": "外部告警", "xpack.securitySolution.hosts.navigation.allHostsTitle": "所有主机", @@ -31230,55 +31209,34 @@ "xpack.watcher.watchActions.webhook.portIsRequiredValidationMessage": "Webhook 端口必填。", "xpack.watcher.watchActions.webhook.usernameIsRequiredIfPasswordValidationMessage": "“用户名”必填。", "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", - "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。", - "unifiedSearch.search.searchBar.savedQueryDescriptionLabelText": "描述", "unifiedSearch.search.searchBar.savedQueryDescriptionText": "保存想要再次使用的查询文本和筛选。", "unifiedSearch.search.searchBar.savedQueryForm.titleConflictText": "标题与现有已保存查询有冲突", - "unifiedSearch.search.searchBar.savedQueryFormCancelButtonText": "取消", "unifiedSearch.search.searchBar.savedQueryFormSaveButtonText": "保存", - "unifiedSearch.search.searchBar.savedQueryFormTitle": "保存查询", "unifiedSearch.search.searchBar.savedQueryIncludeFiltersLabelText": "包括筛选", "unifiedSearch.search.searchBar.savedQueryIncludeTimeFilterLabelText": "包括时间筛选", "unifiedSearch.search.searchBar.savedQueryNameHelpText": "名称必填,其中不能包含前导或尾随空格,并且必须唯一。", "unifiedSearch.search.searchBar.savedQueryNameLabelText": "名称", "unifiedSearch.search.searchBar.savedQueryNoSavedQueriesText": "没有已保存查询。", - "unifiedSearch.search.searchBar.savedQueryPopoverButtonText": "查看已保存查询", - "unifiedSearch.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "清除当前已保存查询", - "unifiedSearch.search.searchBar.savedQueryPopoverClearButtonText": "清除", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "取消", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "删除", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "删除“{savedQueryName}”?", - "unifiedSearch.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "删除已保存查询 {savedQueryName}", "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "另存为新的已保存查询", "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "另存为新的", - "unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "保存新的已保存查询", - "unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonText": "保存当前查询", "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "将更改保存到 {title}", "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText": "保存更改", - "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "已保存查询按钮 {savedQueryName}", - "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName} 描述", - "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "已保存查询按钮已选择 {savedQueryName}。按下可清除任何更改。", - "unifiedSearch.search.searchBar.savedQueryPopoverTitleText": "已保存查询", "unifiedSearch.noDataPopover.content": "此时间范围不包含任何数据。增大或调整时间范围,以查看更多的字段并创建图表。", "unifiedSearch.noDataPopover.dismissAction": "不再显示", "unifiedSearch.noDataPopover.subtitle": "提示", "unifiedSearch.noDataPopover.title": "空数据集", "unifiedSearch.query.queryBar.clearInputLabel": "清除输入", "unifiedSearch.query.queryBar.comboboxAriaLabel": "搜索并筛选 {pageType} 页面", - "unifiedSearch.query.queryBar.kqlFullLanguageName": "Kibana 查询语言", "unifiedSearch.query.queryBar.kqlLanguageName": "KQL", "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoDocLinkText": "文档", "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoOptOutText": "不再显示", "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoText": "似乎您正在查询嵌套字段。您可以使用不同的方式构造嵌套查询的 KQL 语法,具体取决于您想要的结果。详细了解我们的 {link}。", "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoTitle": "KQL 嵌套查询语法", - "unifiedSearch.query.queryBar.kqlOffLabel": "关闭", - "unifiedSearch.query.queryBar.kqlOnLabel": "开启", - "unifiedSearch.query.queryBar.languageSwitcher.toText": "切换到 Kibana 查询语言以进行搜索", "unifiedSearch.query.queryBar.luceneLanguageName": "Lucene", "unifiedSearch.query.queryBar.searchInputAriaLabel": "开始键入内容,以搜索并筛选 {pageType} 页面", - "unifiedSearch.query.queryBar.searchInputPlaceholder": "搜索", - "unifiedSearch.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) 提供简化查询语法并支持脚本字段。KQL 还提供自动完成功能。如果关闭 KQL,{nonKqlModeHelpText}", - "unifiedSearch.query.queryBar.syntaxOptionsDescription.nonKqlModeHelpText": "Kibana 使用 Lucene。", "unifiedSearch.search.unableToGetSavedQueryToastTitle": "无法加载已保存查询 {savedQueryId}", "unifiedSearch.query.queryBar.syntaxOptionsTitle": "语法选项", "unifiedSearch.filter.applyFilterActionTitle": "将筛选应用于当前视图", @@ -31331,22 +31289,18 @@ "unifiedSearch.filter.filterEditor.rangeEndInputPlaceholder": "范围结束", "unifiedSearch.filter.filterEditor.rangeInputLabel": "范围", "unifiedSearch.filter.filterEditor.rangeStartInputPlaceholder": "范围开始", - "unifiedSearch.filter.filterEditor.saveButtonLabel": "保存", "unifiedSearch.filter.filterEditor.trueOptionLabel": "true", "unifiedSearch.filter.filterEditor.valueInputLabel": "值", "unifiedSearch.filter.filterEditor.valueInputPlaceholder": "输入值", "unifiedSearch.filter.filterEditor.valueSelectPlaceholder": "选择值", "unifiedSearch.filter.filterEditor.valuesSelectLabel": "值", "unifiedSearch.filter.filterEditor.valuesSelectPlaceholder": "选择值", - "unifiedSearch.filter.options.changeAllFiltersButtonLabel": "更改所有筛选", - "unifiedSearch.filter.options.deleteAllFiltersButtonLabel": "全部删除", "unifiedSearch.filter.options.disableAllFiltersButtonLabel": "全部禁用", "unifiedSearch.filter.options.enableAllFiltersButtonLabel": "全部启用", - "unifiedSearch.filter.options.invertDisabledFiltersButtonLabel": "反向已启用/已禁用", "unifiedSearch.filter.options.invertNegatedFiltersButtonLabel": "反向包括", "unifiedSearch.filter.options.pinAllFiltersButtonLabel": "全部固定", "unifiedSearch.filter.options.unpinAllFiltersButtonLabel": "全部取消固定", - "unifiedSearch.filter.searchBar.changeAllFiltersTitle": "更改所有筛选", + "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。", "unifiedSearch.kueryAutocomplete.andOperatorDescription": "需要{bothArguments}为 true", "unifiedSearch.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "两个参数都", "unifiedSearch.kueryAutocomplete.equalOperatorDescription": "{equals}某一值", diff --git a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts index 0a0b8cdeab208..33f5fdc44afcd 100644 --- a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts +++ b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts @@ -15,6 +15,7 @@ export const allowedExperimentalValues = Object.freeze({ rulesListDatagrid: true, internalAlertsTable: false, internalShareableComponentsSandbox: false, + ruleTagFilter: false, ruleStatusFilter: false, rulesDetailLogs: true, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 71bcb2ee7d760..c4c273bd003c5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -31,7 +31,7 @@ import { } from '../types'; import { Section, routeToRuleDetails, legacyRouteToRuleDetails } from './constants'; -import { setSavedObjectsClient } from '../common/lib/data_apis'; +import { setDataViewsService } from '../common/lib/data_apis'; import { KibanaContextProvider } from '../common/lib/kibana'; const TriggersActionsUIHome = lazy(() => import('./home')); @@ -67,12 +67,12 @@ export const renderApp = (deps: TriggersAndActionsUiServices) => { }; export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => { - const { savedObjects, uiSettings, theme$ } = deps; + const { dataViews, uiSettings, theme$ } = deps; const sections: Section[] = ['rules', 'connectors', 'alerts', '__components_sandbox']; const isDarkMode = useObservable(uiSettings.get$('theme:darkMode')); const sectionsRegex = sections.join('|'); - setSavedObjectsClient(savedObjects.client); + setDataViewsService(dataViews); return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_tag_filter_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_tag_filter_sandbox.tsx new file mode 100644 index 0000000000000..58603fdb8f178 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_tag_filter_sandbox.tsx @@ -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 React, { useState } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { getRuleTagFilterLazy } from '../../../common/get_rule_tag_filter'; + +export const RuleTagFilterSandbox = () => { + const [selectedTags, setSelectedTags] = useState([]); + + return ( +
+ {getRuleTagFilterLazy({ + tags: ['tag1', 'tag2', 'tag3', 'tag4'], + selectedTags, + onChange: setSelectedTags, + })} + +
selected tags: {JSON.stringify(selectedTags)}
+
+ ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx index bedcbb03045a5..668b1ccb5aa69 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { RuleStatusDropdownSandbox } from './rule_status_dropdown_sandbox'; +import { RuleTagFilterSandbox } from './rule_tag_filter_sandbox'; import { RuleStatusFilterSandbox } from './rule_status_filter_sandbox'; import { RuleTagBadgeSandbox } from './rule_tag_badge_sandbox'; @@ -14,6 +15,7 @@ export const InternalShareableComponentsSandbox: React.FC<{}> = () => { return ( <> + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts index 74b8243519428..bc4957b65b1bc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts @@ -29,3 +29,5 @@ export function hasAllPrivilege(rule: InitialRule, ruleType?: RuleType): boolean export function hasReadPrivilege(rule: InitialRule, ruleType?: RuleType): boolean { return ruleType?.authorizedConsumers[rule.consumer]?.read ?? false; } +export const hasManageApiKeysCapability = (capabilities: Capabilities) => + capabilities?.management?.security?.api_keys; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts index ab8f1b565c888..5377e4269f46e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts @@ -6,7 +6,7 @@ */ import { httpServiceMock } from '@kbn/core/public/mocks'; -import { loadRuleAggregations } from './aggregate'; +import { loadRuleAggregations, loadRuleTags } from './aggregate'; const http = httpServiceMock.createStartContract(); @@ -289,4 +289,68 @@ describe('loadRuleAggregations', () => { ] `); }); + + test('should call aggregate API with tagsFilter', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadRuleAggregations({ + http, + searchText: 'baz', + tagsFilter: ['a', 'b', 'c'], + }); + + expect(result).toEqual({ + ruleExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.tags:(a or b or c)", + "search": "baz", + "search_fields": "[\\"name\\",\\"tags\\"]", + }, + }, + ] + `); + }); + + test('loadRuleTags should call the aggregate API with no filters', async () => { + const resolvedValue = { + rule_tags: ['a', 'b', 'c'], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadRuleTags({ + http, + }); + + expect(result).toEqual({ + ruleTags: ['a', 'b', 'c'], + }); + + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + ] + `); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts index 9548445d0df9c..1df6177443657 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts @@ -10,11 +10,16 @@ import { RuleAggregations, RuleStatus } from '../../../types'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; import { mapFiltersToKql } from './map_filters_to_kql'; +export interface RuleTagsAggregations { + ruleTags: string[]; +} + const rewriteBodyRes: RewriteRequestCase = ({ rule_execution_status: ruleExecutionStatus, rule_enabled_status: ruleEnabledStatus, rule_muted_status: ruleMutedStatus, rule_snoozed_status: ruleSnoozedStatus, + rule_tags: ruleTags, ...rest }: any) => ({ ...rest, @@ -22,8 +27,23 @@ const rewriteBodyRes: RewriteRequestCase = ({ ruleEnabledStatus, ruleMutedStatus, ruleSnoozedStatus, + ruleTags, +}); + +const rewriteTagsBodyRes: RewriteRequestCase = ({ + rule_tags: ruleTags, +}: any) => ({ + ruleTags, }); +// TODO: https://github.com/elastic/kibana/issues/131682 +export async function loadRuleTags({ http }: { http: HttpSetup }): Promise { + const res = await http.get>( + `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate` + ); + return rewriteTagsBodyRes(res); +} + export async function loadRuleAggregations({ http, searchText, @@ -31,6 +51,7 @@ export async function loadRuleAggregations({ actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, }: { http: HttpSetup; searchText?: string; @@ -38,12 +59,14 @@ export async function loadRuleAggregations({ actionTypesFilter?: string[]; ruleExecutionStatusesFilter?: string[]; ruleStatusesFilter?: RuleStatus[]; + tagsFilter?: string[]; }): Promise { const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, }); const res = await http.get>( `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts index 89ede79f4a21d..c9834dd140ea4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts @@ -7,7 +7,7 @@ export { alertingFrameworkHealth } from './health'; export { mapFiltersToKql } from './map_filters_to_kql'; -export { loadRuleAggregations } from './aggregate'; +export { loadRuleAggregations, loadRuleTags } from './aggregate'; export { createRule } from './create'; export { deleteRules } from './delete'; export { disableRule, disableRules } from './disable'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts index df762d05e0eff..f67a27ef5409c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts @@ -88,6 +88,14 @@ describe('mapFiltersToKql', () => { ]); }); + test('should handle tagsFilter', () => { + expect( + mapFiltersToKql({ + tagsFilter: ['a', 'b', 'c'], + }) + ).toEqual(['alert.attributes.tags:(a or b or c)']); + }); + test('should handle typesFilter and actionTypesFilter', () => { expect( mapFiltersToKql({ @@ -100,17 +108,19 @@ describe('mapFiltersToKql', () => { ]); }); - test('should handle typesFilter, actionTypesFilter and ruleExecutionStatusesFilter', () => { + test('should handle typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, and tagsFilter', () => { expect( mapFiltersToKql({ typesFilter: ['type', 'filter'], actionTypesFilter: ['action', 'types', 'filter'], ruleExecutionStatusesFilter: ['alert', 'statuses', 'filter'], + tagsFilter: ['a', 'b', 'c'], }) ).toEqual([ 'alert.attributes.alertTypeId:(type or filter)', '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', 'alert.attributes.executionStatus.status:(alert or statuses or filter)', + 'alert.attributes.tags:(a or b or c)', ]); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts index 0e64f5500454f..ff2a49e3a5e45 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts @@ -25,9 +25,11 @@ export const mapFiltersToKql = ({ actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, }: { typesFilter?: string[]; actionTypesFilter?: string[]; + tagsFilter?: string[]; ruleExecutionStatusesFilter?: string[]; ruleStatusesFilter?: RuleStatus[]; }): string[] => { @@ -68,6 +70,9 @@ export const mapFiltersToKql = ({ filters.push(`${enablementFilter} or ${snoozedFilter}`); } } + if (tagsFilter && tagsFilter.length) { + filters.push(`alert.attributes.tags:(${tagsFilter.join(' or ')})`); + } return filters; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts index 8adc92738b7c6..2a20c9d9469f5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts @@ -336,4 +336,42 @@ describe('loadRules', () => { ] `); }); + + test('should call find API with tagsFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + const result = await loadRules({ + http, + tagsFilter: ['a', 'b', 'c'], + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.tags:(a or b or c)", + "page": 1, + "per_page": 10, + "search": undefined, + "search_fields": undefined, + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts index bdbdcf2f094b2..6e527989cc91f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts @@ -23,6 +23,7 @@ export async function loadRules({ actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, sort = { field: 'name', direction: 'asc' }, }: { http: HttpSetup; @@ -30,6 +31,7 @@ export async function loadRules({ searchText?: string; typesFilter?: string[]; actionTypesFilter?: string[]; + tagsFilter?: string[]; ruleExecutionStatusesFilter?: string[]; ruleStatusesFilter?: RuleStatus[]; sort?: Sorting; @@ -42,6 +44,7 @@ export async function loadRules({ const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, + tagsFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.test.tsx index cbb2ad745a3b2..08b68bd342a5b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.test.tsx @@ -11,16 +11,17 @@ import { AlertsFlyout } from './alerts_flyout'; import { AlertsField } from '../../../../types'; const onClose = jest.fn(); -const onPaginateNext = jest.fn(); -const onPaginatePrevious = jest.fn(); +const onPaginate = jest.fn(); const props = { alert: { [AlertsField.name]: ['one'], [AlertsField.reason]: ['two'], }, + flyoutIndex: 0, + alertsCount: 4, + isLoading: false, onClose, - onPaginateNext, - onPaginatePrevious, + onPaginate, }; describe('AlertsFlyout', () => { @@ -34,19 +35,31 @@ describe('AlertsFlyout', () => { await nextTick(); wrapper.update(); }); - expect(wrapper.find('[data-test-subj="alertsFlyoutTitle"]').first().text()).toBe('one'); + expect(wrapper.find('[data-test-subj="alertsFlyoutName"]').first().text()).toBe('one'); expect(wrapper.find('[data-test-subj="alertsFlyoutReason"]').first().text()).toBe('two'); }); - it('should allow pagination', async () => { + it('should allow pagination with next', async () => { const wrapper = mountWithIntl(); await act(async () => { await nextTick(); wrapper.update(); }); - wrapper.find('[data-test-subj="alertsFlyoutPaginatePrevious"]').first().simulate('click'); - expect(onPaginatePrevious).toHaveBeenCalled(); - wrapper.find('[data-test-subj="alertsFlyoutPaginateNext"]').first().simulate('click'); - expect(onPaginateNext).toHaveBeenCalled(); + wrapper.find('[data-test-subj="pagination-button-next"]').first().simulate('click'); + expect(onPaginate).toHaveBeenCalledWith(1); + }); + + it('should allow pagination with previous', async () => { + const customProps = { + ...props, + flyoutIndex: 1, + }; + const wrapper = mountWithIntl(); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper.find('[data-test-subj="pagination-button-previous"]').first().simulate('click'); + expect(onPaginate).toHaveBeenCalledWith(0); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.tsx index 51174ca7b9a80..44236a8d993f5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_flyout/alerts_flyout.tsx @@ -15,81 +15,111 @@ import { EuiTitle, EuiText, EuiHorizontalRule, - EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem, - EuiButton, + EuiPagination, + EuiProgress, + EuiLoadingContent, } from '@elastic/eui'; import { AlertsField, AlertsData } from '../../../../types'; -const REASON_LABEL = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.reason', +const SAMPLE_TITLE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.sampleTitle', { - defaultMessage: 'Reason', + defaultMessage: 'Sample title', } ); -const NEXT_LABEL = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.next', +const NAME_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.name', { - defaultMessage: 'Next', + defaultMessage: 'Name', } ); -const PREVIOUS_LABEL = i18n.translate( - 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.previous', + +const REASON_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.reason', + { + defaultMessage: 'Reason', + } +); + +const PAGINATION_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.alertsTable.alertsFlyout.paginationLabel', { - defaultMessage: 'Previous', + defaultMessage: 'Alert navigation', } ); interface AlertsFlyoutProps { alert: AlertsData; + flyoutIndex: number; + alertsCount: number; + isLoading: boolean; onClose: () => void; - onPaginateNext: () => void; - onPaginatePrevious: () => void; + onPaginate: (pageIndex: number) => void; } export const AlertsFlyout: React.FunctionComponent = ({ alert, + flyoutIndex, + alertsCount, + isLoading, onClose, - onPaginateNext, - onPaginatePrevious, + onPaginate, }: AlertsFlyoutProps) => { return ( + {isLoading && } - -

{get(alert, AlertsField.name)}

+ +

{SAMPLE_TITLE_LABEL}

+ + + + + +
- -

{REASON_LABEL}

-
- - - {get(alert, AlertsField.reason)} - - - -
- - + - - {PREVIOUS_LABEL} - + +

{NAME_LABEL}

+
+ + {isLoading ? ( + + ) : ( + + {get(alert, AlertsField.name, [])[0]} + + )}
- - {NEXT_LABEL} - + +

{REASON_LABEL}

+
+ + {isLoading ? ( + + ) : ( + + {get(alert, AlertsField.reason, [])[0]} + + )}
-
+ + +
); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx index 6aa3c17220668..6a8c6a0ff9680 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.test.tsx @@ -132,16 +132,16 @@ describe('AlertsTable', () => { const result = await wrapper.findAllByTestId('alertsFlyout'); expect(result.length).toBe(1); - expect(wrapper.queryByTestId('alertsFlyoutTitle')?.textContent).toBe('one'); + expect(wrapper.queryByTestId('alertsFlyoutName')?.textContent).toBe('one'); expect(wrapper.queryByTestId('alertsFlyoutReason')?.textContent).toBe('two'); // Should paginate too - userEvent.click(wrapper.queryAllByTestId('alertsFlyoutPaginateNext')[0]); - expect(wrapper.queryByTestId('alertsFlyoutTitle')?.textContent).toBe('three'); + userEvent.click(wrapper.queryAllByTestId('pagination-button-next')[0]); + expect(wrapper.queryByTestId('alertsFlyoutName')?.textContent).toBe('three'); expect(wrapper.queryByTestId('alertsFlyoutReason')?.textContent).toBe('four'); - userEvent.click(wrapper.queryAllByTestId('alertsFlyoutPaginatePrevious')[0]); - expect(wrapper.queryByTestId('alertsFlyoutTitle')?.textContent).toBe('one'); + userEvent.click(wrapper.queryAllByTestId('pagination-button-previous')[0]); + expect(wrapper.queryByTestId('alertsFlyoutName')?.textContent).toBe('one'); expect(wrapper.queryByTestId('alertsFlyoutReason')?.textContent).toBe('two'); }); @@ -152,10 +152,10 @@ describe('AlertsTable', () => { const result = await wrapper.findAllByTestId('alertsFlyout'); expect(result.length).toBe(1); - userEvent.click(wrapper.queryAllByTestId('alertsFlyoutPaginateNext')[0]); + userEvent.click(wrapper.queryAllByTestId('pagination-button-next')[0]); expect(fetchAlertsData.onPageChange).toHaveBeenCalledWith({ pageIndex: 1, pageSize: 1 }); - userEvent.click(wrapper.queryAllByTestId('alertsFlyoutPaginatePrevious')[0]); + userEvent.click(wrapper.queryAllByTestId('pagination-button-previous')[0]); expect(fetchAlertsData.onPageChange).toHaveBeenCalledWith({ pageIndex: 0, pageSize: 1 }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx index da05b4c175bdd..dca547e65ae27 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/alerts_table.tsx @@ -39,14 +39,14 @@ const emptyConfiguration = { const AlertsTable: React.FunctionComponent = (props: AlertsTableProps) => { const [rowClasses, setRowClasses] = useState({}); - const { activePage, alertsCount, onPageChange, onSortChange } = props.useFetchAlertsData(); + const { activePage, alertsCount, onPageChange, onSortChange, isLoading } = + props.useFetchAlertsData(); const { sortingColumns, onSort } = useSorting(onSortChange); const { pagination, onChangePageSize, onChangePageIndex, - onPaginateFlyoutNext, - onPaginateFlyoutPrevious, + onPaginateFlyout, flyoutAlertIndex, setFlyoutAlertIndex, } = usePagination({ @@ -122,9 +122,11 @@ const AlertsTable: React.FunctionComponent = (props: AlertsTab )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.test.ts index 6073d907f161d..70bc4c4ade8fb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.test.ts @@ -58,31 +58,31 @@ describe('usePagination', () => { expect(result.current.flyoutAlertIndex).toBe(-1); act(() => { - result.current.onPaginateFlyoutNext(); + result.current.onPaginateFlyout(0); }); expect(result.current.flyoutAlertIndex).toBe(0); act(() => { - result.current.onPaginateFlyoutNext(); + result.current.onPaginateFlyout(1); }); expect(result.current.flyoutAlertIndex).toBe(1); act(() => { - result.current.onPaginateFlyoutPrevious(); + result.current.onPaginateFlyout(0); }); expect(result.current.flyoutAlertIndex).toBe(0); }); - it('should paginate the flyout when we need to change the page index', () => { + it('should paginate the flyout when we need to change the page index going back', () => { const { result } = renderHook(() => usePagination({ onPageChange, pageIndex: 0, pageSize: 1, alertsCount }) ); act(() => { - result.current.onPaginateFlyoutPrevious(); + result.current.onPaginateFlyout(-2); }); // It should reset to the first alert in the table @@ -90,25 +90,21 @@ describe('usePagination', () => { // It should go to the last page expect(result.current.pagination).toStrictEqual({ pageIndex: 4, pageSize: 1 }); + }); - act(() => { - result.current.onPaginateFlyoutNext(); - }); - - // It should reset to the first alert in the table - expect(result.current.flyoutAlertIndex).toBe(0); - - // It should go to the first page - expect(result.current.pagination).toStrictEqual({ pageIndex: 0, pageSize: 1 }); + it('should paginate the flyout when we need to change the page index going forward', () => { + const { result } = renderHook(() => + usePagination({ onPageChange, pageIndex: 0, pageSize: 1, alertsCount }) + ); act(() => { - result.current.onPaginateFlyoutNext(); + result.current.onPaginateFlyout(1); }); // It should reset to the first alert in the table expect(result.current.flyoutAlertIndex).toBe(0); - // It should go to the second page + // It should go to the first page expect(result.current.pagination).toStrictEqual({ pageIndex: 1, pageSize: 1 }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts index 484775d9877dd..76f4f0fa546c4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/use_pagination.ts @@ -58,19 +58,20 @@ export function usePagination({ onPageChange, pageIndex, pageSize, alertsCount } }, [pagination, alertsCount, onChangePageIndex] ); - const onPaginateFlyoutNext = useCallback(() => { - paginateFlyout(flyoutAlertIndex + 1); - }, [paginateFlyout, flyoutAlertIndex]); - const onPaginateFlyoutPrevious = useCallback(() => { - paginateFlyout(flyoutAlertIndex - 1); - }, [paginateFlyout, flyoutAlertIndex]); + + const onPaginateFlyout = useCallback( + (nextPageIndex: number) => { + nextPageIndex -= pagination.pageSize * pagination.pageIndex; + paginateFlyout(nextPageIndex); + }, + [paginateFlyout, pagination.pageSize, pagination.pageIndex] + ); return { pagination, onChangePageSize, onChangePageIndex, - onPaginateFlyoutNext, - onPaginateFlyoutPrevious, + onPaginateFlyout, flyoutAlertIndex, setFlyoutAlertIndex, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx index e41c2a73a5124..9ab31ae12402f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx @@ -32,6 +32,9 @@ export const ActionForm = suspendedComponentWithProps( export const RuleStatusDropdown = suspendedComponentWithProps( lazy(() => import('./rules_list/components/rule_status_dropdown')) ); +export const RuleTagFilter = suspendedComponentWithProps( + lazy(() => import('./rules_list/components/rule_tag_filter')) +); export const RuleStatusFilter = suspendedComponentWithProps( lazy(() => import('./rules_list/components/rule_status_filter')) ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx index fe17dde8c1282..7857eb172eedb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx @@ -48,6 +48,7 @@ jest.mock('../../../lib/capabilities', () => ({ hasAllPrivilege: jest.fn(() => true), hasSaveRulesCapability: jest.fn(() => true), hasExecuteActionsCapability: jest.fn(() => true), + hasManageApiKeysCapability: jest.fn(() => true), })); const useKibanaMock = useKibana as jest.Mocked; const ruleTypeRegistry = ruleTypeRegistryMock.create(); @@ -100,6 +101,26 @@ describe('rule_details', () => { ).toBeTruthy(); }); + it('renders the API key owner badge when user can manage API keys', () => { + const rule = mockRule(); + expect( + shallow( + + ).find({rule.apiKeyOwner}) + ).toBeTruthy(); + }); + + it(`doesn't render the API key owner badge when user can't manage API keys`, () => { + const { hasManageApiKeysCapability } = jest.requireMock('../../../lib/capabilities'); + hasManageApiKeysCapability.mockReturnValueOnce(false); + const rule = mockRule(); + expect( + shallow() + .find({rule.apiKeyOwner}) + .exists() + ).toBeFalsy(); + }); + it('renders the rule error banner with error message, when rule has a license error', () => { const rule = mockRule({ enabled: true, @@ -871,7 +892,7 @@ function mockRule(overloads: Partial = {}): Rule { updatedBy: null, createdAt: new Date(), updatedAt: new Date(), - apiKeyOwner: null, + apiKeyOwner: 'bob', throttle: null, notifyWhen: null, muteAll: false, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx index b3363159851d0..0389e6b0d9b30 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx @@ -27,7 +27,11 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { RuleExecutionStatusErrorReasons, parseDuration } from '@kbn/alerting-plugin/common'; -import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; +import { + hasAllPrivilege, + hasExecuteActionsCapability, + hasManageApiKeysCapability, +} from '../../../lib/capabilities'; import { getAlertingSectionBreadcrumb, getRuleDetailsBreadcrumb } from '../../../lib/breadcrumb'; import { getCurrentDocTitle } from '../../../lib/doc_title'; import { @@ -310,6 +314,27 @@ export const RuleDetails: React.FunctionComponent = ({
+ {hasManageApiKeysCapability(capabilities) ? ( + + + + +

+ +

+
+
+ + + {rule.apiKeyOwner} + + +
+
+ ) : null} {uniqueActions && uniqueActions.length ? ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx new file mode 100644 index 0000000000000..a6b60b1099391 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx @@ -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 React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { EuiFilterButton, EuiSelectable } from '@elastic/eui'; +import { RuleTagFilter } from './rule_tag_filter'; + +const onChangeMock = jest.fn(); + +const tags = ['a', 'b', 'c', 'd', 'e', 'f']; + +describe('rule_tag_filter', () => { + beforeEach(() => { + onChangeMock.mockReset(); + }); + + it('renders correctly', () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find(EuiFilterButton).exists()).toBeTruthy(); + expect(wrapper.find('.euiNotificationBadge').text()).toEqual('0'); + }); + + it('can open the popover correctly', () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('[data-test-subj="ruleTagFilterSelectable"]').exists()).toBeFalsy(); + + wrapper.find(EuiFilterButton).simulate('click'); + + expect(wrapper.find('[data-test-subj="ruleTagFilterSelectable"]').exists()).toBeTruthy(); + expect(wrapper.find('li').length).toEqual(tags.length); + }); + + it('can select tags', () => { + const wrapper = mountWithIntl( + + ); + + wrapper.find(EuiFilterButton).simulate('click'); + + wrapper.find('[data-test-subj="ruleTagFilterOption-a"]').at(0).simulate('click'); + expect(onChangeMock).toHaveBeenCalledWith(['a']); + + wrapper.setProps({ + selectedTags: ['a'], + }); + + wrapper.find('[data-test-subj="ruleTagFilterOption-a"]').at(0).simulate('click'); + expect(onChangeMock).toHaveBeenCalledWith([]); + + wrapper.find('[data-test-subj="ruleTagFilterOption-b"]').at(0).simulate('click'); + expect(onChangeMock).toHaveBeenCalledWith(['a', 'b']); + }); + + it('renders selected tags even if they get deleted from the tags array', () => { + const selectedTags = ['g', 'h']; + const wrapper = mountWithIntl( + + ); + + wrapper.find(EuiFilterButton).simulate('click'); + + expect(wrapper.find(EuiSelectable).props().options.length).toEqual( + tags.length + selectedTags.length + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx new file mode 100644 index 0000000000000..6aa8aa8c69213 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useState, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiSelectable, + EuiFilterGroup, + EuiFilterButton, + EuiPopover, + EuiSelectableProps, + EuiSelectableOption, + EuiSpacer, +} from '@elastic/eui'; + +export interface RuleTagFilterProps { + tags: string[]; + selectedTags: string[]; + isLoading?: boolean; + loadingMessage?: EuiSelectableProps['loadingMessage']; + noMatchesMessage?: EuiSelectableProps['noMatchesMessage']; + emptyMessage?: EuiSelectableProps['emptyMessage']; + errorMessage?: EuiSelectableProps['errorMessage']; + dataTestSubj?: string; + selectableDataTestSubj?: string; + optionDataTestSubj?: (tag: string) => string; + buttonDataTestSubj?: string; + onChange: (tags: string[]) => void; +} + +const getOptionDataTestSubj = (tag: string) => `ruleTagFilterOption-${tag}`; + +export const RuleTagFilter = (props: RuleTagFilterProps) => { + const { + tags = [], + selectedTags = [], + isLoading = false, + loadingMessage, + noMatchesMessage, + emptyMessage, + errorMessage, + dataTestSubj = 'ruleTagFilter', + selectableDataTestSubj = 'ruleTagFilterSelectable', + optionDataTestSubj = getOptionDataTestSubj, + buttonDataTestSubj = 'ruleTagFilterButton', + onChange = () => {}, + } = props; + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const allTags = useMemo(() => { + return [...new Set([...tags, ...selectedTags])].sort(); + }, [tags, selectedTags]); + + const options: EuiSelectableOption[] = useMemo( + () => + allTags.map((tag) => ({ + label: tag, + checked: selectedTags.includes(tag) ? 'on' : undefined, + 'data-test-subj': optionDataTestSubj(tag), + })), + [allTags, selectedTags, optionDataTestSubj] + ); + + const onChangeInternal = useCallback( + (newOptions: EuiSelectableOption[]) => { + const newSelectedTags = newOptions.reduce((result, option) => { + if (option.checked === 'on') { + result = [...result, option.label]; + } + return result; + }, []); + + onChange(newSelectedTags); + }, + [onChange] + ); + + const onClosePopover = () => { + setIsPopoverOpen(!isPopoverOpen); + }; + + const renderButton = () => { + return ( + 0} + numActiveFilters={selectedTags.length} + numFilters={selectedTags.length} + onClick={onClosePopover} + > + + + ); + }; + + return ( + + + + {(list, search) => ( + <> + {search} + + {list} + + )} + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { RuleTagFilter as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index 52c6e2d3ed149..12e1b0f1e4a6e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -33,6 +33,7 @@ jest.mock('../../../lib/rule_api', () => ({ loadRules: jest.fn(), loadRuleTypes: jest.fn(), loadRuleAggregations: jest.fn(), + loadRuleTags: jest.fn(), alertingFrameworkHealth: jest.fn(() => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true, @@ -63,7 +64,10 @@ jest.mock('../../../lib/capabilities', () => ({ jest.mock('../../../../common/get_experimental_features', () => ({ getIsExperimentalFeatureEnabled: jest.fn(), })); -const { loadRules, loadRuleTypes, loadRuleAggregations } = + +const ruleTags = ['a', 'b', 'c', 'd']; + +const { loadRules, loadRuleTypes, loadRuleAggregations, loadRuleTags } = jest.requireMock('../../../lib/rule_api'); const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -395,6 +399,10 @@ describe('rules_list component with items', () => { ruleEnabledStatus: { enabled: 2, disabled: 0 }, ruleExecutionStatus: { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 }, ruleMutedStatus: { muted: 0, unmuted: 2 }, + ruleTags, + }); + loadRuleTags.mockResolvedValue({ + ruleTags, }); const ruleTypeMock: RuleTypeModel = { @@ -842,6 +850,40 @@ describe('rules_list component with items', () => { expect(loadRules.mock.calls[3][0].ruleStatusesFilter).toEqual(['enabled']); }); + + it('does not render the tag filter is the feature flag is off', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="ruleTagFilter"]').exists()).toBeFalsy(); + }); + + it('renders the tag filter if the experiment is on', async () => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); + await setup(); + expect(wrapper.find('[data-test-subj="ruleTagFilter"]').exists()).toBeTruthy(); + }); + + it('can filter by tags', async () => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); + loadRules.mockReset(); + await setup(); + + expect(loadRules.mock.calls[0][0].tagsFilter).toEqual([]); + + wrapper.find('[data-test-subj="ruleTagFilterButton"] button').simulate('click'); + + const tagFilterListItems = wrapper.find( + '[data-test-subj="ruleTagFilterSelectable"] .euiSelectableListItem' + ); + expect(tagFilterListItems.length).toEqual(ruleTags.length); + + tagFilterListItems.at(0).simulate('click'); + + expect(loadRules.mock.calls[1][0].tagsFilter).toEqual(['a']); + + tagFilterListItems.at(1).simulate('click'); + + expect(loadRules.mock.calls[2][0].tagsFilter).toEqual(['a', 'b']); + }); }); describe('rules_list component empty with show only capability', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index b1255600b68de..a5b9661835131 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -73,6 +73,7 @@ import { RuleExecutionStatusFilter, getHealthColor } from './rule_execution_stat import { loadRules, loadRuleAggregations, + loadRuleTags, loadRuleTypes, disableRule, enableRule, @@ -99,6 +100,7 @@ import { RuleDurationFormat } from './rule_duration_format'; import { shouldShowDurationWarning } from '../../../lib/execution_duration_utils'; import { getFormattedSuccessRatio } from '../../../lib/monitoring_utils'; import { triggersActionsUiConfig } from '../../../../common/lib/config_api'; +import { RuleTagFilter } from './rule_tag_filter'; import { RuleStatusFilter } from './rule_status_filter'; import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; @@ -158,6 +160,8 @@ export const RulesList: React.FunctionComponent = () => { const [actionTypesFilter, setActionTypesFilter] = useState([]); const [ruleExecutionStatusesFilter, setRuleExecutionStatusesFilter] = useState([]); const [ruleStatusesFilter, setRuleStatusesFilter] = useState([]); + const [tags, setTags] = useState([]); + const [tagsFilter, setTagsFilter] = useState([]); const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); @@ -167,6 +171,7 @@ export const RulesList: React.FunctionComponent = () => { ); const [showErrors, setShowErrors] = useState(false); + const isRuleTagFilterEnabled = getIsExperimentalFeatureEnabled('ruleTagFilter'); const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter'); useEffect(() => { @@ -233,6 +238,7 @@ export const RulesList: React.FunctionComponent = () => { JSON.stringify(actionTypesFilter), JSON.stringify(ruleExecutionStatusesFilter), JSON.stringify(ruleStatusesFilter), + JSON.stringify(tagsFilter), ]); useEffect(() => { @@ -293,8 +299,10 @@ export const RulesList: React.FunctionComponent = () => { actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, sort, }); + await loadRuleTagsAggs(); await loadRuleAggs(); setRulesState({ isLoading: false, @@ -311,7 +319,8 @@ export const RulesList: React.FunctionComponent = () => { isEmpty(typesFilter) && isEmpty(actionTypesFilter) && isEmpty(ruleExecutionStatusesFilter) && - isEmpty(ruleStatusesFilter) + isEmpty(ruleStatusesFilter) && + isEmpty(tagsFilter) ); setNoData(rulesResponse.data.length === 0 && !isFilterApplied); @@ -339,6 +348,7 @@ export const RulesList: React.FunctionComponent = () => { actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, }); if (rulesAggs?.ruleExecutionStatus) { setRulesStatusesTotal(rulesAggs.ruleExecutionStatus); @@ -355,6 +365,24 @@ export const RulesList: React.FunctionComponent = () => { } } + async function loadRuleTagsAggs() { + if (!isRuleTagFilterEnabled) { + return; + } + try { + const ruleTagsAggs = await loadRuleTags({ http }); + if (ruleTagsAggs?.ruleTags) { + setTags(ruleTagsAggs.ruleTags); + } + } catch (e) { + toasts.addDanger({ + title: i18n.translate('xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTags', { + defaultMessage: 'Unable to load rule tags', + }), + }); + } + } + const renderRuleStatusDropdown = (ruleEnabled: boolean | undefined, item: RuleTableItem) => { return ( { sortable: false, width: '50px', 'data-test-subj': 'rulesTableCell-tagsPopover', - render: (tags: string[], item: RuleTableItem) => { - return tags.length > 0 ? ( + render: (ruleTags: string[], item: RuleTableItem) => { + return ruleTags.length > 0 ? ( setTagPopoverOpenIndex(item.index)} onClose={() => setTagPopoverOpenIndex(-1)} /> @@ -940,6 +968,13 @@ export const RulesList: React.FunctionComponent = () => { ); }; + const getRuleTagFilter = () => { + if (isRuleTagFilterEnabled) { + return []; + } + return []; + }; + const getRuleStatusFilter = () => { if (isRuleStatusFilterEnabled) { return [ @@ -960,6 +995,7 @@ export const RulesList: React.FunctionComponent = () => { }) )} />, + ...getRuleTagFilter(), ...getRuleStatusFilter(), { rulesListDatagrid: true, internalAlertsTable: true, rulesDetailLogs: true, + ruleTagFilter: true, ruleStatusFilter: true, internalShareableComponentsSandbox: true, }, @@ -39,6 +40,10 @@ describe('getIsExperimentalFeatureEnabled', () => { expect(result).toEqual(true); + result = getIsExperimentalFeatureEnabled('ruleTagFilter'); + + expect(result).toEqual(true); + result = getIsExperimentalFeatureEnabled('ruleStatusFilter'); expect(result).toEqual(true); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_filter.tsx new file mode 100644 index 0000000000000..ccca277ef10ba --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_filter.tsx @@ -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 React from 'react'; +import { RuleTagFilter } from '../application/sections'; +import type { RuleTagFilterProps } from '../application/sections/rules_list/components/rule_tag_filter'; + +export const getRuleTagFilterLazy = (props: RuleTagFilterProps) => { + return ; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.test.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.test.ts index 9b0d122e24d4e..178c891dc3a34 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.test.ts @@ -7,7 +7,7 @@ import { loadIndexPatterns, - setSavedObjectsClient, + setDataViewsService, getMatchingIndices, getESIndexFields, } from './data_apis'; @@ -19,10 +19,8 @@ const http = httpServiceMock.createStartContract(); const pattern = 'test-pattern'; const indexes = ['test-index']; -const generateIndexPattern = (title: string) => ({ - attributes: { - title, - }, +const generateDataView = (title: string) => ({ + title, }); const mockIndices = { indices: ['indices1', 'indices2'] }; @@ -67,7 +65,7 @@ describe('Data API', () => { describe('index patterns', () => { beforeEach(() => { - setSavedObjectsClient({ + setDataViewsService({ find: mockFind, }); }); @@ -76,68 +74,15 @@ describe('Data API', () => { }); test('fetches the index patterns', async () => { - mockFind.mockResolvedValueOnce({ - savedObjects: [generateIndexPattern('index-1'), generateIndexPattern('index-2')], - total: 2, - }); + mockFind.mockResolvedValueOnce([generateDataView('index-1'), generateDataView('index-2')]); const results = await loadIndexPatterns(mockPattern); expect(mockFind).toBeCalledTimes(1); - expect(mockFind).toBeCalledWith({ - fields: ['title'], - page: 1, - perPage, - search: '*test-pattern*', - type: 'index-pattern', - }); + expect(mockFind).toBeCalledWith('*test-pattern*', perPage); expect(results).toEqual(['index-1', 'index-2']); }); - test(`fetches the index patterns as chunks and merges them, if the total number of index patterns more than ${perPage}`, async () => { - mockFind.mockResolvedValueOnce({ - savedObjects: [generateIndexPattern('index-1'), generateIndexPattern('index-2')], - total: 2010, - }); - mockFind.mockResolvedValueOnce({ - savedObjects: [generateIndexPattern('index-3'), generateIndexPattern('index-4')], - total: 2010, - }); - mockFind.mockResolvedValueOnce({ - savedObjects: [generateIndexPattern('index-5'), generateIndexPattern('index-6')], - total: 2010, - }); - const results = await loadIndexPatterns(mockPattern); - - expect(mockFind).toBeCalledTimes(3); - expect(mockFind).toHaveBeenNthCalledWith(1, { - fields: ['title'], - page: 1, - perPage, - search: '*test-pattern*', - type: 'index-pattern', - }); - expect(mockFind).toHaveBeenNthCalledWith(2, { - fields: ['title'], - page: 2, - perPage, - search: '*test-pattern*', - type: 'index-pattern', - }); - expect(mockFind).toHaveBeenNthCalledWith(3, { - fields: ['title'], - page: 3, - perPage, - search: '*test-pattern*', - type: 'index-pattern', - }); - expect(results).toEqual(['index-1', 'index-2', 'index-3', 'index-4', 'index-5', 'index-6']); - }); - - test('returns an empty array if one of the requests fails', async () => { - mockFind.mockResolvedValueOnce({ - savedObjects: [generateIndexPattern('index-1'), generateIndexPattern('index-2')], - total: 1010, - }); + test('returns an empty array if find requests fails', async () => { mockFind.mockRejectedValueOnce(500); const results = await loadIndexPatterns(mockPattern); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts index 55b1ef4be2c74..90f80dd3dc2f0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts @@ -6,6 +6,7 @@ */ import { HttpSetup } from '@kbn/core/public'; +import { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; const DATA_API_ROOT = '/api/triggers_actions_ui/data'; @@ -62,57 +63,25 @@ export async function getESIndexFields({ return fields; } -let savedObjectsClient: any; +type DataViewsService = Pick; +let dataViewsService: DataViewsService; -export const setSavedObjectsClient = (aSavedObjectsClient: any) => { - savedObjectsClient = aSavedObjectsClient; +export const setDataViewsService = (aDataViewsService: DataViewsService) => { + dataViewsService = aDataViewsService; }; -export const getSavedObjectsClient = () => { - return savedObjectsClient; +export const getDataViewsService = () => { + return dataViewsService; }; export const loadIndexPatterns = async (pattern: string) => { - let allSavedObjects = []; const formattedPattern = formatPattern(pattern); const perPage = 1000; try { - const { savedObjects, total } = await getSavedObjectsClient().find({ - type: 'index-pattern', - fields: ['title'], - page: 1, - search: formattedPattern, - perPage, - }); + const dataViews: DataView[] = await getDataViewsService().find(formattedPattern, perPage); - allSavedObjects = savedObjects; - - if (total > perPage) { - let currentPage = 2; - const numberOfPages = Math.ceil(total / perPage); - const promises = []; - - while (currentPage <= numberOfPages) { - promises.push( - getSavedObjectsClient().find({ - type: 'index-pattern', - page: currentPage, - fields: ['title'], - search: formattedPattern, - perPage, - }) - ); - currentPage++; - } - - const paginatedResults = await Promise.all(promises); - - allSavedObjects = paginatedResults.reduce((oldResult, result) => { - return oldResult.concat(result.savedObjects); - }, allSavedObjects); - } - return allSavedObjects.map((indexPattern: any) => indexPattern.attributes.title); + return dataViews.map((dataView: DataView) => dataView.title); } catch (e) { return []; } diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 1e109b96ae5c9..82923205666f6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -18,6 +18,7 @@ export type { RuleType, RuleTypeIndex, RuleTypeModel, + RuleStatus, ActionType, ActionTypeRegistryContract, RuleTypeRegistryContract, diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts index cb79a1509a6c1..003748f7d421e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts @@ -26,6 +26,7 @@ import { } from './types'; import { getAlertsTableLazy } from './common/get_alerts_table'; import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown'; +import { getRuleTagFilterLazy } from './common/get_rule_tag_filter'; import { getRuleStatusFilterLazy } from './common/get_rule_status_filter'; import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge'; @@ -66,6 +67,9 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { getRuleStatusDropdown: (props) => { return getRuleStatusDropdownLazy(props); }, + getRuleTagFilter: (props) => { + return getRuleTagFilterLazy(props); + }, getRuleStatusFilter: (props) => { return getRuleStatusFilterLazy(props); }, diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 1d9c3c07e44ca..c95dd73102fd9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -31,6 +31,7 @@ import { getAddAlertFlyoutLazy } from './common/get_add_alert_flyout'; import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout'; import { getAlertsTableLazy } from './common/get_alerts_table'; import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown'; +import { getRuleTagFilterLazy } from './common/get_rule_tag_filter'; import { getRuleStatusFilterLazy } from './common/get_rule_status_filter'; import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge'; import { ExperimentalFeaturesService } from './common/experimental_features_service'; @@ -48,6 +49,7 @@ import type { ConnectorEditFlyoutProps, AlertsTableProps, RuleStatusDropdownProps, + RuleTagFilterProps, RuleStatusFilterProps, RuleTagBadgeProps, AlertsTableConfigurationRegistry, @@ -80,6 +82,7 @@ export interface TriggersAndActionsUIPublicPluginStart { ) => ReactElement; getAlertsTable: (props: AlertsTableProps) => ReactElement; getRuleStatusDropdown: (props: RuleStatusDropdownProps) => ReactElement; + getRuleTagFilter: (props: RuleTagFilterProps) => ReactElement; getRuleStatusFilter: (props: RuleStatusFilterProps) => ReactElement; getRuleTagBadge: (props: RuleTagBadgeProps) => ReactElement; } @@ -255,6 +258,9 @@ export class Plugin getRuleStatusDropdown: (props: RuleStatusDropdownProps) => { return getRuleStatusDropdownLazy(props); }, + getRuleTagFilter: (props: RuleTagFilterProps) => { + return getRuleTagFilterLazy(props); + }, getRuleStatusFilter: (props: RuleStatusFilterProps) => { return getRuleStatusFilterLazy(props); }, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 25efbfb6ecc38..ef7ea7096961b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -48,6 +48,7 @@ import { import { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/common'; import { TypeRegistry } from './application/type_registry'; import type { ComponentOpts as RuleStatusDropdownProps } from './application/sections/rules_list/components/rule_status_dropdown'; +import type { RuleTagFilterProps } from './application/sections/rules_list/components/rule_tag_filter'; import type { RuleStatusFilterProps } from './application/sections/rules_list/components/rule_status_filter'; import type { RuleTagBadgeProps } from './application/sections/rules_list/components/rule_tag_badge'; @@ -82,6 +83,7 @@ export type { ResolvedRule, SanitizedRule, RuleStatusDropdownProps, + RuleTagFilterProps, RuleStatusFilterProps, RuleTagBadgeProps, }; diff --git a/x-pack/plugins/ux/public/services/rest/create_call_apm_api.ts b/x-pack/plugins/ux/public/services/rest/create_call_apm_api.ts index eddd5fe07694e..f7e9b6799e17d 100644 --- a/x-pack/plugins/ux/public/services/rest/create_call_apm_api.ts +++ b/x-pack/plugins/ux/public/services/rest/create_call_apm_api.ts @@ -8,16 +8,11 @@ import { CoreSetup, CoreStart } from '@kbn/core/public'; import type { ClientRequestParamsOf, - formatRequest as formatRequestType, ReturnOf, RouteRepositoryClient, ServerRouteRepository, } from '@kbn/server-route-repository'; -// @ts-expect-error cannot find module or correspondent type declarations -// The code and types are at separated folders on @kbn/server-route-repository -// so in order to do targeted imports they must me imported separately, and -// an error is expected here -import { formatRequest } from '@kbn/server-route-repository/target_node/format_request'; +import { formatRequest } from '@kbn/server-route-repository'; import type { APMServerRouteRepository, APIEndpoint, @@ -73,10 +68,7 @@ export function createCallApmApi(core: CoreStart | CoreSetup) { params?: Partial>; }; - const { method, pathname } = formatRequest( - endpoint, - params?.path - ) as ReturnType; + const { method, pathname } = formatRequest(endpoint, params?.path); return callApi(core, { ...options, diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js deleted file mode 100644 index ee99785aa8fad..0000000000000 --- a/x-pack/scripts/functional_tests.js +++ /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. - */ - -require('../../src/setup_node_env'); -require('@kbn/test').runTestsCli([ - require.resolve('../test/functional/config.ccs.ts'), - require.resolve('../test/functional/config.js'), - require.resolve('../test/functional_basic/config.ts'), - require.resolve('../test/security_solution_endpoint/config.ts'), - require.resolve('../test/plugin_functional/config.ts'), - require.resolve('../test/functional_with_es_ssl/config.ts'), - require.resolve('../test/functional/config_security_basic.ts'), - require.resolve('../test/reporting_functional/reporting_and_security.config.ts'), - require.resolve('../test/reporting_functional/reporting_without_security.config.ts'), - require.resolve('../test/reporting_functional/reporting_and_deprecated_security.config.ts'), - require.resolve('../test/security_functional/login_selector.config.ts'), - require.resolve('../test/security_functional/oidc.config.ts'), - require.resolve('../test/security_functional/saml.config.ts'), - require.resolve('../test/functional_embedded/config.ts'), - require.resolve('../test/functional_cors/config.ts'), - require.resolve('../test/functional_enterprise_search/without_host_configured.config.ts'), - require.resolve('../test/saved_object_tagging/functional/config.ts'), - require.resolve('../test/usage_collection/config.ts'), - require.resolve('../test/fleet_functional/config.ts'), - require.resolve('../test/functional_synthetics/config.js'), - require.resolve('../test/api_integration/config_security_basic.ts'), - require.resolve('../test/api_integration/config_security_trial.ts'), - require.resolve('../test/api_integration/config.ts'), - require.resolve('../test/api_integration_basic/config.ts'), - require.resolve('../test/alerting_api_integration/basic/config.ts'), - require.resolve('../test/alerting_api_integration/spaces_only/config.ts'), - require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), - require.resolve('../test/cases_api_integration/security_and_spaces/config_basic.ts'), - require.resolve('../test/cases_api_integration/security_and_spaces/config_trial.ts'), - require.resolve('../test/cases_api_integration/spaces_only/config.ts'), - require.resolve('../test/apm_api_integration/basic/config.ts'), - require.resolve('../test/apm_api_integration/trial/config.ts'), - require.resolve('../test/apm_api_integration/rules/config.ts'), - require.resolve('../test/detection_engine_api_integration/security_and_spaces/config.ts'), - require.resolve('../test/detection_engine_api_integration/basic/config.ts'), - require.resolve('../test/lists_api_integration/security_and_spaces/config.ts'), - require.resolve('../test/plugin_api_integration/config.ts'), - require.resolve('../test/rule_registry/security_and_spaces/config_basic.ts'), - require.resolve('../test/rule_registry/security_and_spaces/config_trial.ts'), - require.resolve('../test/rule_registry/spaces_only/config_basic.ts'), - require.resolve('../test/rule_registry/spaces_only/config_trial.ts'), - require.resolve('../test/security_api_integration/saml.config.ts'), - require.resolve('../test/security_api_integration/session_idle.config.ts'), - require.resolve('../test/security_api_integration/session_invalidate.config.ts'), - require.resolve('../test/security_api_integration/session_lifespan.config.ts'), - require.resolve('../test/security_api_integration/login_selector.config.ts'), - require.resolve('../test/security_api_integration/audit.config.ts'), - require.resolve('../test/security_api_integration/http_bearer.config.ts'), - require.resolve('../test/security_api_integration/http_no_auth_providers.config.ts'), - require.resolve('../test/security_api_integration/kerberos.config.ts'), - require.resolve('../test/security_api_integration/kerberos_anonymous_access.config.ts'), - require.resolve('../test/security_api_integration/pki.config.ts'), - require.resolve('../test/security_api_integration/oidc.config.ts'), - require.resolve('../test/security_api_integration/oidc_implicit_flow.config.ts'), - require.resolve('../test/security_api_integration/token.config.ts'), - require.resolve('../test/security_api_integration/anonymous.config.ts'), - require.resolve('../test/security_api_integration/anonymous_es_anonymous.config.ts'), - require.resolve('../test/observability_api_integration/basic/config.ts'), - require.resolve('../test/observability_api_integration/trial/config.ts'), - require.resolve('../test/observability_functional/with_rac_write.config.ts'), - require.resolve('../test/encrypted_saved_objects_api_integration/config.ts'), - require.resolve('../test/spaces_api_integration/spaces_only/config.ts'), - require.resolve('../test/spaces_api_integration/security_and_spaces/config_trial.ts'), - require.resolve('../test/spaces_api_integration/security_and_spaces/config_basic.ts'), - require.resolve('../test/saved_object_api_integration/security_and_spaces/config_trial.ts'), - require.resolve('../test/saved_object_api_integration/security_and_spaces/config_basic.ts'), - require.resolve('../test/saved_object_api_integration/spaces_only/config.ts'), - // TODO: Enable once RBAC timeline search strategy - // tests updated - // require.resolve('../test/timeline/security_and_spaces/config_basic.ts'), - require.resolve('../test/timeline/security_and_spaces/config_trial.ts'), - require.resolve('../test/ui_capabilities/security_and_spaces/config.ts'), - require.resolve('../test/ui_capabilities/spaces_only/config.ts'), - require.resolve('../test/upgrade_assistant_integration/config.js'), - require.resolve('../test/licensing_plugin/config.ts'), - require.resolve('../test/licensing_plugin/config.public.ts'), - require.resolve('../test/endpoint_api_integration_no_ingest/config.ts'), - require.resolve('../test/reporting_api_integration/reporting_and_security.config.ts'), - require.resolve('../test/reporting_api_integration/reporting_without_security.config.ts'), - require.resolve('../test/security_solution_endpoint_api_int/config.ts'), - require.resolve('../test/fleet_api_integration/config.ts'), - require.resolve('../test/search_sessions_integration/config.ts'), - require.resolve('../test/saved_object_tagging/api_integration/security_and_spaces/config.ts'), - require.resolve('../test/saved_object_tagging/api_integration/tagging_api/config.ts'), - require.resolve('../test/examples/config.ts'), - require.resolve('../test/functional_execution_context/config.ts'), -]); diff --git a/x-pack/scripts/functional_tests_server.js b/x-pack/scripts/functional_tests_server.js index 946f7ea3836a6..329fea019221b 100755 --- a/x-pack/scripts/functional_tests_server.js +++ b/x-pack/scripts/functional_tests_server.js @@ -8,4 +8,4 @@ process.env.ALLOW_PERFORMANCE_HOOKS_IN_TASK_MANAGER = true; require('../../src/setup_node_env'); -require('@kbn/test').startServersCli(require.resolve('../test/functional/config.js')); +require('@kbn/test').startServersCli(require.resolve('../test/functional/config.base.js')); diff --git a/x-pack/tasks/build.ts b/x-pack/tasks/build.ts index 4680464976d79..dacb6b6447aff 100644 --- a/x-pack/tasks/build.ts +++ b/x-pack/tasks/build.ts @@ -11,6 +11,7 @@ import { writeFileSync } from 'fs'; import { promisify } from 'util'; import { pipeline } from 'stream'; +import { discoverBazelPackages } from '@kbn/bazel-packages'; import { REPO_ROOT } from '@kbn/utils'; import { transformFileStream, transformFileWithBabel } from '@kbn/dev-utils'; import { ToolingLog } from '@kbn/tooling-log'; @@ -49,6 +50,11 @@ async function reportTask() { } async function copySourceAndBabelify() { + // get bazel packages inside x-pack + const xpackBazelPackages = (await discoverBazelPackages()) + .filter((pkg) => pkg.normalizedRepoRelativeDir.startsWith('x-pack/')) + .map((pkg) => `${pkg.normalizedRepoRelativeDir.replace('x-pack/', '')}/**`); + // copy source files and apply some babel transformations in the process await asyncPipeline( vfs.src( @@ -87,6 +93,7 @@ async function copySourceAndBabelify() { 'plugins/apm/ftr_e2e/**', 'plugins/apm/scripts/**', 'plugins/lists/server/scripts/**', + ...xpackBazelPackages, ], allowEmpty: true, } diff --git a/x-pack/test/accessibility/apps/advanced_settings.ts b/x-pack/test/accessibility/apps/advanced_settings.ts index 6f2dc78a7b35b..6c931f0a0e5a1 100644 --- a/x-pack/test/accessibility/apps/advanced_settings.ts +++ b/x-pack/test/accessibility/apps/advanced_settings.ts @@ -13,7 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const toasts = getService('toasts'); - describe('Stack Management -Advanced Settings', () => { + describe('Stack Management -Advanced Settings Accessibility', () => { // click on Management > Advanced settings it('click on advanced settings ', async () => { await PageObjects.common.navigateToUrl('management', 'kibana/settings', { diff --git a/x-pack/test/accessibility/apps/canvas.ts b/x-pack/test/accessibility/apps/canvas.ts index 609c8bf5bb1ae..d9508e75bdf27 100644 --- a/x-pack/test/accessibility/apps/canvas.ts +++ b/x-pack/test/accessibility/apps/canvas.ts @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const { common } = getPageObjects(['common']); - describe('Canvas', () => { + describe('Canvas Accessibility', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/canvas/default'); await common.navigateToApp('canvas'); diff --git a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts index 5624a5f25db2f..20b72e142f5c7 100644 --- a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts +++ b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts @@ -20,7 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PANEL_TITLE = 'Visualization PieChart'; - describe('Dashboard Edit Panel', () => { + describe('Dashboard Edit Panel Accessibility', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/dashboard/drilldowns'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); diff --git a/x-pack/test/accessibility/apps/enterprise_search.ts b/x-pack/test/accessibility/apps/enterprise_search.ts index aa6910842b5eb..0a1a5d68d9621 100644 --- a/x-pack/test/accessibility/apps/enterprise_search.ts +++ b/x-pack/test/accessibility/apps/enterprise_search.ts @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const { common } = getPageObjects(['common']); - describe('Enterprise Search', () => { + describe('Enterprise Search Accessibility', () => { // NOTE: These accessibility tests currently only run against Enterprise Search in Kibana // without a sidecar Enterprise Search service/host configured, and as such only test // the basic setup guides and not the full application(s) diff --git a/x-pack/test/accessibility/apps/grok_debugger.ts b/x-pack/test/accessibility/apps/grok_debugger.ts index ecb62ffd53177..8ee9114c7da0a 100644 --- a/x-pack/test/accessibility/apps/grok_debugger.ts +++ b/x-pack/test/accessibility/apps/grok_debugger.ts @@ -12,8 +12,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const grokDebugger = getService('grokDebugger'); - // this test is failing as there is a violation https://github.com/elastic/kibana/issues/62102 - describe.skip('Dev tools grok debugger', () => { + // Fixes:https://github.com/elastic/kibana/issues/62102 + describe('Dev tools grok debugger', () => { before(async () => { await PageObjects.common.navigateToApp('grokDebugger'); await grokDebugger.assertExists(); diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/home.ts index 61297859c29f8..544a32843f7f3 100644 --- a/x-pack/test/accessibility/apps/home.ts +++ b/x-pack/test/accessibility/apps/home.ts @@ -13,7 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); - describe('Kibana Home', () => { + describe('Kibana Home Accessibility', () => { before(async () => { await common.navigateToApp('home'); }); diff --git a/x-pack/test/accessibility/apps/index_lifecycle_management.ts b/x-pack/test/accessibility/apps/index_lifecycle_management.ts index 6cec8d1cb891a..fc3ec1ff5cf81 100644 --- a/x-pack/test/accessibility/apps/index_lifecycle_management.ts +++ b/x-pack/test/accessibility/apps/index_lifecycle_management.ts @@ -63,14 +63,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { throw new Error(`Could not find ${policyName} in policy table`); }; - describe('Index Lifecycle Management', async () => { + describe('Index Lifecycle Management Accessibility', async () => { before(async () => { await esClient.snapshot.createRepository({ name: REPO_NAME, body: { type: 'fs', settings: { - // use one of the values defined in path.repo in test/functional/config.js + // use one of the values defined in path.repo in test/functional/config.base.js location: '/tmp/', }, }, diff --git a/x-pack/test/accessibility/apps/ingest_node_pipelines.ts b/x-pack/test/accessibility/apps/ingest_node_pipelines.ts index a09a07bb01267..4bbd9cde06d2d 100644 --- a/x-pack/test/accessibility/apps/ingest_node_pipelines.ts +++ b/x-pack/test/accessibility/apps/ingest_node_pipelines.ts @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }: any) { const log = getService('log'); const a11y = getService('a11y'); /* this is the wrapping service around axe */ - describe('Ingest Pipelines', async () => { + describe('Ingest Pipelines Accessibility', async () => { before(async () => { await putSamplePipeline(esClient); await common.navigateToApp('ingestPipelines'); diff --git a/x-pack/test/accessibility/apps/kibana_overview.ts b/x-pack/test/accessibility/apps/kibana_overview.ts index 9d21f08a900cc..19af9c2828d35 100644 --- a/x-pack/test/accessibility/apps/kibana_overview.ts +++ b/x-pack/test/accessibility/apps/kibana_overview.ts @@ -11,7 +11,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'home']); const a11y = getService('a11y'); - describe('Kibana overview', () => { + describe('Kibana overview Accessibility', () => { const esArchiver = getService('esArchiver'); before(async () => { diff --git a/x-pack/test/accessibility/apps/lens.ts b/x-pack/test/accessibility/apps/lens.ts index 8a46d662a61cf..18459b56c0542 100644 --- a/x-pack/test/accessibility/apps/lens.ts +++ b/x-pack/test/accessibility/apps/lens.ts @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); const kibanaServer = getService('kibanaServer'); - describe('Lens', () => { + describe('Lens Accessibility', () => { const lensChartName = 'MyLensChart'; before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/logstash_functional'); diff --git a/x-pack/test/accessibility/apps/license_management.ts b/x-pack/test/accessibility/apps/license_management.ts index 891a682e653ba..7693ebb197ff1 100644 --- a/x-pack/test/accessibility/apps/license_management.ts +++ b/x-pack/test/accessibility/apps/license_management.ts @@ -12,7 +12,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const testSubjects = getService('testSubjects'); - describe('License Management page a11y tests', () => { + describe('License Management page Accessibility', () => { before(async () => { await PageObjects.common.navigateToApp('licenseManagement'); }); diff --git a/x-pack/test/accessibility/apps/login_page.ts b/x-pack/test/accessibility/apps/login_page.ts index 154517d09502e..6463e63fb2e49 100644 --- a/x-pack/test/accessibility/apps/login_page.ts +++ b/x-pack/test/accessibility/apps/login_page.ts @@ -14,8 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'security']); - // Failing: See https://github.com/elastic/kibana/issues/96372 - describe('Security', () => { + describe('Security Accessibility', () => { describe('Login Page', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); diff --git a/x-pack/test/accessibility/apps/maps.ts b/x-pack/test/accessibility/apps/maps.ts index c5b824c330829..0e4142e6ade60 100644 --- a/x-pack/test/accessibility/apps/maps.ts +++ b/x-pack/test/accessibility/apps/maps.ts @@ -13,7 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'settings', 'header', 'home', 'maps']); - describe('Maps app meets ally validations', () => { + describe('Maps app Accessibility', () => { before(async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/ml.ts index fd05d2af07747..2b99b665daced 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/ml.ts @@ -5,15 +5,13 @@ * 2.0. */ -import path from 'path'; - import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const a11y = getService('a11y'); const ml = getService('ml'); - describe('ml', () => { + describe('ml Accessibility', () => { const esArchiver = getService('esArchiver'); before(async () => { @@ -79,16 +77,8 @@ export default function ({ getService }: FtrProviderContext) { const dfaJobType = 'outlier_detection'; const dfaJobId = `ihp_ally_${Date.now()}`; - const uploadFilePath = path.join( - __dirname, - '..', - '..', - 'functional', - 'apps', - 'ml', - 'data_visualizer', - 'files_to_import', - 'artificial_server_log' + const uploadFilePath = require.resolve( + '../../functional/apps/ml/data_visualizer/files_to_import/artificial_server_log' ); before(async () => { @@ -261,8 +251,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.selectJobType(dfaJobType); await ml.testExecution.logTestStep('displays the source data preview'); await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewExists(); - await ml.testExecution.logTestStep('enables the source data preview histogram charts'); - await ml.dataFrameAnalyticsCreation.enableSourceDataPreviewHistogramCharts(true); + await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewHistogramChartEnabled(true); await ml.testExecution.logTestStep('displays the include fields selection'); await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists(); await a11y.testAppSnapshot(); diff --git a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts b/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts index 9532e8e365655..8f8bc67304c0d 100644 --- a/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts +++ b/x-pack/test/accessibility/apps/ml_embeddables_in_dashboard.ts @@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'timePicker', 'dashboard']); const a11y = getService('a11y'); /* this is the wrapping service around axe */ - describe('machine learning embeddables anomaly charts', function () { + describe('machine learning embeddables anomaly charts Accessibility', function () { before(async () => { await ml.securityCommon.createMlRoles(); await ml.securityCommon.createMlUsers(); diff --git a/x-pack/test/accessibility/apps/painless_lab.ts b/x-pack/test/accessibility/apps/painless_lab.ts index c25930941b5eb..a0a4712dbe4e3 100644 --- a/x-pack/test/accessibility/apps/painless_lab.ts +++ b/x-pack/test/accessibility/apps/painless_lab.ts @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const retry = getService('retry'); - describe('Accessibility Painless Lab Editor', () => { + describe('Accessibility Painless Lab Editor Accessibility', () => { before(async () => { await PageObjects.common.navigateToApp('painlessLab'); }); diff --git a/x-pack/test/accessibility/apps/remote_clusters.ts b/x-pack/test/accessibility/apps/remote_clusters.ts index 67c85eda60a4a..deb0e4a090b8c 100644 --- a/x-pack/test/accessibility/apps/remote_clusters.ts +++ b/x-pack/test/accessibility/apps/remote_clusters.ts @@ -79,7 +79,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const retry = getService('retry'); - describe('Remote Clusters', () => { + describe('Remote Clusters Accessibility', () => { beforeEach(async () => { await PageObjects.common.navigateToApp('remoteClusters'); }); diff --git a/x-pack/test/accessibility/apps/reporting.ts b/x-pack/test/accessibility/apps/reporting.ts index c6a6571cc0ff6..f1ac0770c9587 100644 --- a/x-pack/test/accessibility/apps/reporting.ts +++ b/x-pack/test/accessibility/apps/reporting.ts @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const security = getService('security'); const log = getService('log'); - describe('Reporting', () => { + describe('Reporting Accessibility', () => { const createReportingUser = async () => { await security.user.create(reporting.REPORTING_USER_USERNAME, { password: reporting.REPORTING_USER_PASSWORD, diff --git a/x-pack/test/accessibility/apps/roles.ts b/x-pack/test/accessibility/apps/roles.ts index 3c40e664d7da2..5369dced427fa 100644 --- a/x-pack/test/accessibility/apps/roles.ts +++ b/x-pack/test/accessibility/apps/roles.ts @@ -17,7 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const kibanaServer = getService('kibanaServer'); - describe('Kibana roles page a11y tests', () => { + describe('Kibana roles page Accessibility', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.uiSettings.update({ diff --git a/x-pack/test/accessibility/apps/search_profiler.ts b/x-pack/test/accessibility/apps/search_profiler.ts index 47909662fb132..30043f8f4157f 100644 --- a/x-pack/test/accessibility/apps/search_profiler.ts +++ b/x-pack/test/accessibility/apps/search_profiler.ts @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const esArchiver = getService('esArchiver'); - describe('Accessibility Search Profiler Editor', () => { + describe('Search Profiler Editor Accessibility', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await PageObjects.common.navigateToApp('searchProfiler'); diff --git a/x-pack/test/accessibility/apps/search_sessions.ts b/x-pack/test/accessibility/apps/search_sessions.ts index 30bef9086a4b6..42a2f387612ac 100644 --- a/x-pack/test/accessibility/apps/search_sessions.ts +++ b/x-pack/test/accessibility/apps/search_sessions.ts @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); const esArchiver = getService('esArchiver'); - describe('Search sessions a11y tests', () => { + describe('Search sessions Accessibility', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/data/search_sessions'); await PageObjects.searchSessionsManagement.goTo(); diff --git a/x-pack/test/accessibility/apps/security_solution.ts b/x-pack/test/accessibility/apps/security_solution.ts index 8014e03152b17..ef930f093eb4a 100644 --- a/x-pack/test/accessibility/apps/security_solution.ts +++ b/x-pack/test/accessibility/apps/security_solution.ts @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); // FLAKY: https://github.com/elastic/kibana/issues/95707 - describe.skip('Security Solution', () => { + describe.skip('Security Solution Accessibility', () => { before(async () => { await security.testUser.setRoles(['superuser'], { skipBrowserRefresh: true }); await common.navigateToApp('security'); diff --git a/x-pack/test/accessibility/apps/spaces.ts b/x-pack/test/accessibility/apps/spaces.ts index 567f958f5f8a4..38b34054911f6 100644 --- a/x-pack/test/accessibility/apps/spaces.ts +++ b/x-pack/test/accessibility/apps/spaces.ts @@ -18,7 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const toasts = getService('toasts'); - describe('Kibana spaces page meets a11y validations', () => { + describe('Kibana Spaces Accessibility', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); await PageObjects.common.navigateToApp('home'); diff --git a/x-pack/test/accessibility/apps/tags.ts b/x-pack/test/accessibility/apps/tags.ts index 8174c8fa8c06b..da51f2f0535e2 100644 --- a/x-pack/test/accessibility/apps/tags.ts +++ b/x-pack/test/accessibility/apps/tags.ts @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const toasts = getService('toasts'); - describe('Kibana tags page meets a11y validations', () => { + describe('Kibana Tags Page Accessibility', () => { before(async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, diff --git a/x-pack/test/accessibility/apps/transform.ts b/x-pack/test/accessibility/apps/transform.ts index 59f19471490b8..fa54ea4ad6766 100644 --- a/x-pack/test/accessibility/apps/transform.ts +++ b/x-pack/test/accessibility/apps/transform.ts @@ -11,7 +11,7 @@ export default function ({ getService }: FtrProviderContext) { const a11y = getService('a11y'); const transform = getService('transform'); - describe('transform', () => { + describe('transform Accessibility', () => { const esArchiver = getService('esArchiver'); before(async () => { diff --git a/x-pack/test/accessibility/apps/upgrade_assistant.ts b/x-pack/test/accessibility/apps/upgrade_assistant.ts index 1f7fd2a654bca..fffb6e684ba4a 100644 --- a/x-pack/test/accessibility/apps/upgrade_assistant.ts +++ b/x-pack/test/accessibility/apps/upgrade_assistant.ts @@ -53,7 +53,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const es = getService('es'); const log = getService('log'); - describe.skip('Upgrade Assistant', () => { + describe.skip('Upgrade Assistant Accessibility', () => { before(async () => { await PageObjects.upgradeAssistant.navigateToPage(); diff --git a/x-pack/test/accessibility/apps/uptime.ts b/x-pack/test/accessibility/apps/uptime.ts index 41664c5920b82..49243c37fe730 100644 --- a/x-pack/test/accessibility/apps/uptime.ts +++ b/x-pack/test/accessibility/apps/uptime.ts @@ -19,7 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const es = getService('es'); const toasts = getService('toasts'); - describe('uptime', () => { + describe('uptime Accessibility', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/uptime/blank'); await makeChecks(es, A11Y_TEST_MONITOR_ID, 150, 1, 1000, { diff --git a/x-pack/test/accessibility/apps/users.ts b/x-pack/test/accessibility/apps/users.ts index 8682cc8f0a884..5833a19580c24 100644 --- a/x-pack/test/accessibility/apps/users.ts +++ b/x-pack/test/accessibility/apps/users.ts @@ -17,7 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); const retry = getService('retry'); - describe('Kibana users page a11y tests', () => { + describe('Kibana users Accessibility', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); await PageObjects.security.clickElasticsearchUsers(); diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index e85b8a9ef17d8..30c130df23a15 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -10,7 +10,7 @@ import { services } from './services'; import { pageObjects } from './page_objects'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { ...functionalConfig.getAll(), diff --git a/x-pack/test/alerting_api_integration/basic/tests/index.ts b/x-pack/test/alerting_api_integration/basic/tests/index.ts index 6ddb09b1c666e..ba6ebbe6a944e 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/index.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/index.ts @@ -13,8 +13,6 @@ export default function alertingApiIntegrationTests({ getService, }: FtrProviderContext) { describe('alerting api integration basic license', function () { - this.tags('ciGroup13'); - loadTestFile(require.resolve('./actions')); loadTestFile(require.resolve('./alerts')); }); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 14039ad3360a0..ffdf0c09ad216 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -25,6 +25,7 @@ interface CreateTestConfigOptions { customizeLocalHostSsl?: boolean; rejectUnauthorized?: boolean; // legacy emailDomainsAllowed?: string[]; + testFiles?: string[]; } // test.not-enabled is specifically not enabled @@ -64,6 +65,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) customizeLocalHostSsl = false, rejectUnauthorized = true, // legacy emailDomainsAllowed = undefined, + testFiles = undefined, } = options; return async ({ readConfigFile }: FtrConfigProviderContext) => { @@ -139,7 +141,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) : []; return { - testFiles: [require.resolve(`../${name}/tests/`)], + testFiles: testFiles ? testFiles : [require.resolve(`../${name}/tests/`)], servers, services, junit: { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index 4525768a0fb42..82516bf4a417d 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -15,6 +15,7 @@ import { ActionType } from '@kbn/actions-plugin/server'; import { initPlugin as initPagerduty } from './pagerduty_simulation'; import { initPlugin as initSwimlane } from './swimlane_simulation'; import { initPlugin as initServiceNow } from './servicenow_simulation'; +import { initPlugin as initServiceNowOAuth } from './servicenow_oauth_simulation'; import { initPlugin as initJira } from './jira_simulation'; import { initPlugin as initResilient } from './resilient_simulation'; import { initPlugin as initSlack } from './slack_simulation'; @@ -49,6 +50,7 @@ export function getAllExternalServiceSimulatorPaths(): string[] { allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.RESILIENT}/rest/orgs/201/incidents`); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.MS_EXCHANGE}/users/test@/sendMail`); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.MS_EXCHANGE}/1234567/oauth2/v2.0/token`); + allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/oauth_token.do`); return allPaths; } @@ -129,6 +131,10 @@ export class FixturePlugin implements Plugin, + res: KibanaResponseFactory + ): Promise> { + return jsonResponse(res, 200, { + access_token: 'tokentokentoken', + expires_in: 3660, + token_type: 'Bearer', + }); + } + ); +} + +function jsonResponse( + res: KibanaResponseFactory, + code: number, + object: Record = {} +) { + return res.custom>({ body: object, statusCode: code }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/config.ts similarity index 82% rename from x-pack/test/alerting_api_integration/security_and_spaces/config.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/config.ts index 314f65c167048..9b90a4c18fdf0 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/config.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createTestConfig } from '../common/config'; +import { createTestConfig } from '../../common/config'; // eslint-disable-next-line import/no-default-export export default createTestConfig('security_and_spaces', { @@ -14,4 +14,5 @@ export default createTestConfig('security_and_spaces', { ssl: true, enableActionsProxy: true, publicBaseUrl: true, + testFiles: [require.resolve('./tests')], }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts index 57ba6e3863576..e601c6ee15ec7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; +import { UserAtSpaceScenarios } from '../../../scenarios'; import { checkAAD, getTestRuleData, @@ -15,8 +15,8 @@ import { ObjectRemover, getProducerUnauthorizedErrorMessage, TaskManagerDoc, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/delete.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/delete.ts index 56c50f035b10e..2b6086cf38d92 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/delete.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; +import { UserAtSpaceScenarios } from '../../../scenarios'; import { getUrlPrefix, getTestRuleData, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, ObjectRemover, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createDeleteTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts index 8a4266eb8dc8a..864de743ea343 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createDisableAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/enable.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/enable.ts index 6d667eff24072..0aba468174cff 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/enable.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -17,7 +17,7 @@ import { getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, TaskManagerDoc, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createEnableAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/execution_status.ts similarity index 95% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/execution_status.ts index 4e61fd6593113..122353f444536 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/execution_status.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/execution_status.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; import { RuleExecutionStatusErrorReasons } from '@kbn/alerting-plugin/common'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function executionStatusAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts index 84f0d7709d01a..20a5e82d303fe 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts @@ -9,9 +9,9 @@ import expect from '@kbn/expect'; import { SuperTest, Test } from 'supertest'; import { chunk, omit } from 'lodash'; import uuid from 'uuid'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; const findTestUtils = ( describeType: 'internal' | 'public', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts index 180a3cf36e27f..48559aa35ac3c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts @@ -7,15 +7,15 @@ import expect from '@kbn/expect'; import { SuperTest, Test } from 'supertest'; -import { UserAtSpaceScenarios } from '../../scenarios'; +import { UserAtSpaceScenarios } from '../../../scenarios'; import { getUrlPrefix, getTestRuleData, ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; const getTestUtils = ( describeType: 'internal' | 'public', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_state.ts similarity index 97% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_state.ts index 3bdfe49464fcf..e3da329c1cbaf 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_state.ts @@ -12,9 +12,9 @@ import { getTestRuleData, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { UserAtSpaceScenarios } from '../../scenarios'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; // eslint-disable-next-line import/no-default-export export default function createGetAlertStateTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts similarity index 97% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts index eb4e592a91d8a..f0ba9dc451937 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts @@ -13,9 +13,9 @@ import { getTestRuleData, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { UserAtSpaceScenarios } from '../../scenarios'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; // eslint-disable-next-line import/no-default-export export default function createGetAlertSummaryTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts new file mode 100644 index 0000000000000..6753b6383872d --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { setupSpacesAndUsers, tearDown } from '../../../setup'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { + describe('Alerts - Group 1', () => { + describe('alerts', () => { + before(async () => { + await setupSpacesAndUsers(getService); + }); + + after(async () => { + await tearDown(getService); + }); + + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./create')); + loadTestFile(require.resolve('./delete')); + loadTestFile(require.resolve('./disable')); + loadTestFile(require.resolve('./enable')); + loadTestFile(require.resolve('./execution_status')); + loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./get_alert_state')); + loadTestFile(require.resolve('./get_alert_summary')); + loadTestFile(require.resolve('./rule_types')); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/rule_types.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/rule_types.ts index 0c527ac1449f8..e9d8175f9066e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/rule_types.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; import { omit } from 'lodash'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix } from '../../../common/lib/space_test_utils'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { getUrlPrefix } from '../../../../common/lib/space_test_utils'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function listAlertTypes({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/index.ts new file mode 100644 index 0000000000000..795af26627dfb --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('alerting api integration security and spaces enabled', function () { + describe('', function () { + loadTestFile(require.resolve('./alerting')); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts new file mode 100644 index 0000000000000..9b90a4c18fdf0 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/config.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 { createTestConfig } from '../../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + disabledPlugins: [], + license: 'trial', + ssl: true, + enableActionsProxy: true, + publicBaseUrl: true, + testFiles: [require.resolve('./tests')], +}); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/email.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/email.ts index 6fb2315956b69..4d9282d4fdeea 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/email.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/email.ts @@ -7,11 +7,11 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { ExternalServiceSimulator, getExternalServiceSimulatorPath, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function emailTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/es_index.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/es_index.ts index edf352936e979..fed3acba1147e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/es_index.ts @@ -7,7 +7,7 @@ import type { Client } from '@elastic/elasticsearch'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; const ES_TEST_INDEX_NAME = 'functional-test-actions-index'; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/es_index_preconfigured.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/es_index_preconfigured.ts index caa7d57688037..a447921ac5041 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/es_index_preconfigured.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/es_index_preconfigured.ts @@ -7,7 +7,7 @@ import type { Client } from '@elastic/elasticsearch'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; // from: x-pack/test/alerting_api_integration/common/config.ts const ACTION_ID = 'preconfigured-es-index-action'; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/jira.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/jira.ts index 33185a20c9249..5a6e0967736a2 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/jira.ts @@ -8,13 +8,13 @@ import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function jiraTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/oauth_access_token.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/oauth_access_token.ts new file mode 100644 index 0000000000000..6053f78ea76a4 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/oauth_access_token.ts @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import fs from 'fs'; +import expect from '@kbn/expect'; +import { promisify } from 'util'; +import httpProxy from 'http-proxy'; +import { KBN_KEY_PATH } from '@kbn/dev-utils'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; + +// eslint-disable-next-line import/no-default-export +export default function oAuthAccessTokenTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + + describe('get oauth access token', () => { + let servicenowSimulatorURL: string = ''; + let proxyServer: httpProxy | undefined; + let testPrivateKey: string; + const configService = getService('config'); + + // need to wait for kibanaServer to settle ... + before(async () => { + testPrivateKey = await promisify(fs.readFile)(KBN_KEY_PATH, 'utf8'); + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + proxyServer = await getHttpProxyServer( + kibanaServer.resolveUrl('/'), + configService.get('kbnTestServer.serverArgs'), + () => {} + ); + }); + + after(() => { + if (proxyServer) { + proxyServer.close(); + } + }); + + it('should return 200 when requesting a JWT access token with OAuth credentials', async () => { + const { body: accessToken } = await supertest + .post('/internal/actions/connector/_oauth_access_token') + .set('kbn-xsrf', 'foo') + .send({ + type: 'jwt', + options: { + tokenUrl: `${servicenowSimulatorURL}/oauth_token.do`, + config: { + clientId: 'abc', + userIdentifierValue: 'elastic', + jwtKeyId: 'def', + }, + secrets: { + clientSecret: 'xyz', + privateKey: testPrivateKey, + }, + }, + }) + .expect(200); + + expect(accessToken).to.eql({ accessToken: 'Bearer tokentokentoken' }); + }); + + it('should return 200 when requesting a Client Credentials access token with OAuth credentials', async () => { + const { body: accessToken } = await supertest + .post('/internal/actions/connector/_oauth_access_token') + .set('kbn-xsrf', 'foo') + .send({ + type: 'client', + options: { + tokenUrl: `${kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.MS_EXCHANGE) + )}/1234567/oauth2/v2.0/token`, + scope: 'https://graph.microsoft.com/.default', + config: { + clientId: 'abc', + tenantId: '98765', + }, + secrets: { + clientSecret: 'xyz', + }, + }, + }) + .expect(200); + + expect(accessToken).to.eql({ accessToken: 'Bearer asdadasd' }); + }); + + it('should return 400 when given incorrect options for requesting Client Credentials access token with OAuth credentials', async () => { + await supertest + .post('/internal/actions/connector/_oauth_access_token') + .set('kbn-xsrf', 'foo') + .send({ + type: 'client', + options: { + tokenUrl: `${servicenowSimulatorURL}/oauth_token.do`, + config: { + clientId: 'abc', + userIdentifierValue: 'elastic', + jwtKeyId: 'def', + }, + secrets: { + clientSecret: 'xyz', + privateKey: testPrivateKey, + }, + }, + }) + .expect(400); + }); + + it('should return 400 when given incorrect options for requesting JWT access token with OAuth credentials', async () => { + await supertest + .post('/internal/actions/connector/_oauth_access_token') + .set('kbn-xsrf', 'foo') + .send({ + type: 'jwt', + options: { + tokenUrl: `${kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.MS_EXCHANGE) + )}/1234567/oauth2/v2.0/token`, + scope: 'https://graph.microsoft.com/.default', + config: { + clientId: 'abc', + tenantId: '98765', + }, + secrets: { + clientSecret: 'xyz', + }, + }, + }) + .expect(400); + }); + + it('should return 400 when token url not included in allowlist', async () => { + const { body } = await supertest + .post('/internal/actions/connector/_oauth_access_token') + .set('kbn-xsrf', 'foo') + .send({ + type: 'jwt', + options: { + tokenUrl: `https://servicenow.nonexistent.com/oauth_token.do`, + config: { + clientId: 'abc', + userIdentifierValue: 'elastic', + jwtKeyId: 'def', + }, + secrets: { + clientSecret: 'xyz', + privateKey: testPrivateKey, + }, + }, + }); + + expect(body.statusCode).to.equal(400); + expect(body.message).to.equal( + `target url "https://servicenow.nonexistent.com/oauth_token.do" is not added to the Kibana config xpack.actions.allowedHosts` + ); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/pagerduty.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/pagerduty.ts index 05dba49236197..731eab7dad0f3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/pagerduty.ts @@ -8,13 +8,13 @@ import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function pagerdutyTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/resilient.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/resilient.ts index e2a92701b62cb..fec22ab72e1ef 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/resilient.ts @@ -8,13 +8,13 @@ import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function resilientTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/server_log.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/server_log.ts index fb7bac7d81e9c..b8b12d6aac764 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/server_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/server_log.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function serverLogTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itom.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_itom.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itom.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_itom.ts index c685fff8abfc6..0771e4e293726 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itom.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_itom.ts @@ -11,9 +11,9 @@ import { asyncForEach } from '@kbn/std'; import getPort from 'get-port'; import http from 'http'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getServiceNowServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { getServiceNowServer } from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function serviceNowITOMTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_itsm.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_itsm.ts index 0f81753bbc731..368e1b104a87e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_itsm.ts @@ -11,9 +11,9 @@ import { asyncForEach } from '@kbn/std'; import getPort from 'get-port'; import http from 'http'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getServiceNowServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { getServiceNowServer } from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function serviceNowITSMTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_sir.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_sir.ts index 0f5640f7edd3e..f08ca542e4617 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/servicenow_sir.ts @@ -11,9 +11,9 @@ import { asyncForEach } from '@kbn/std'; import getPort from 'get-port'; import http from 'http'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getServiceNowServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { getServiceNowServer } from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function serviceNowSIRTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/slack.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/slack.ts index 66b988fb9b4eb..a05c622d8a1cf 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/slack.ts @@ -9,10 +9,10 @@ import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; import http from 'http'; import getPort from 'get-port'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getSlackServer } from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/swimlane.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/swimlane.ts index a55e8e30d419a..4119a409d7a4b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/swimlane.ts @@ -10,9 +10,9 @@ import expect from '@kbn/expect'; import getPort from 'get-port'; import http from 'http'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getSwimlaneServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { getSwimlaneServer } from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function swimlaneTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/webhook.ts similarity index 97% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/webhook.ts index 44a74a7a31571..c484dfad69539 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/webhook.ts @@ -10,13 +10,13 @@ import http from 'http'; import expect from '@kbn/expect'; import { URL, format as formatUrl } from 'url'; import getPort from 'get-port'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, getWebhookServer, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; const defaultValues: Record = { headers: null, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/xmatters.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/xmatters.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/xmatters.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/xmatters.ts index 7ce357bc62e36..e33597057cfe1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/xmatters.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/xmatters.ts @@ -8,13 +8,13 @@ import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; -import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function xmattersTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.ts new file mode 100644 index 0000000000000..506d0016af4f6 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.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 { createTestConfig } from '../../../../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + disabledPlugins: [], + license: 'trial', + ssl: true, + enableActionsProxy: true, + publicBaseUrl: true, + testFiles: [require.resolve('.')], +}); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types.ts similarity index 91% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types.ts index ec23719880926..feacbaa48be42 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/connector_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/connector_types.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix } from '../../../common/lib/space_test_utils'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { getUrlPrefix } from '../../../../common/lib/space_test_utils'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function listActionTypesTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts index 06e8017177138..15d43f9782d94 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/create.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createActionTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts similarity index 97% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts index a0aae0a1bd64d..b0cfffdd2f464 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/delete.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { getUrlPrefix, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function deleteActionTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/execute.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/execute.ts index c8433ac60575f..35361920cc729 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/execute.ts @@ -7,15 +7,15 @@ import expect from '@kbn/expect'; import { IValidatedEvent, nanosToMillis } from '@kbn/event-log-plugin/server'; -import { UserAtSpaceScenarios } from '../../scenarios'; +import { UserAtSpaceScenarios } from '../../../scenarios'; import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix, ObjectRemover, getEventLog, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get.ts similarity index 96% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get.ts index 9842b13a9745d..f58481bb94412 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { getUrlPrefix, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function getActionTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts index ab6181369755f..103ae5abd3071 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/get_all.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function getAllActionTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts similarity index 89% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts index 93d4bbb4065ed..9c1b6a4fd8299 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { setupSpacesAndUsers, tearDown } from '..'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { setupSpacesAndUsers, tearDown } from '../../../setup'; // eslint-disable-next-line import/no-default-export export default function actionsTests({ loadTestFile, getService }: FtrProviderContext) { @@ -25,6 +25,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./builtin_action_types/pagerduty')); loadTestFile(require.resolve('./builtin_action_types/swimlane')); loadTestFile(require.resolve('./builtin_action_types/server_log')); + loadTestFile(require.resolve('./builtin_action_types/oauth_access_token')); loadTestFile(require.resolve('./builtin_action_types/servicenow_itsm')); loadTestFile(require.resolve('./builtin_action_types/servicenow_sir')); loadTestFile(require.resolve('./builtin_action_types/servicenow_itom')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/manual/pr_40694.js b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/manual/pr_40694.js similarity index 100% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/manual/pr_40694.js rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/manual/pr_40694.js diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts index 92bb7084ec534..6a9181d5ba5dd 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/update.ts @@ -6,9 +6,9 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function updateActionTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts index 407d6467296e4..5dfbdfa9707c2 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/alerts.ts @@ -11,8 +11,8 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IValidatedEvent, nanosToMillis } from '@kbn/event-log-plugin/server'; import { TaskRunning, TaskRunningStage } from '@kbn/task-manager-plugin/server/task_running'; import { ConcreteTaskInstance } from '@kbn/task-manager-plugin/server'; -import { UserAtSpaceScenarios, Superuser } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios, Superuser } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ESTestIndexTool, ES_TEST_INDEX_NAME, @@ -23,7 +23,7 @@ import { getConsumerUnauthorizedErrorMessage, TaskManagerUtils, getEventLog, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function alertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/event_log.ts similarity index 92% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/event_log.ts index 4a572002a4366..424c734b5c6a2 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/event_log.ts @@ -6,10 +6,10 @@ */ import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { validateEvent } from '../../../spaces_only/tests/alerting/event_log'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { validateEvent } from '../../../../spaces_only/tests/alerting/event_log'; // eslint-disable-next-line import/no-default-export export default function eventLogTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/excluded.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/excluded.ts similarity index 94% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/excluded.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/excluded.ts index eae80da85dc59..d09edae045e6f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/excluded.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/excluded.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; +import { UserAtSpaceScenarios } from '../../../scenarios'; import { getTestRuleData, getUrlPrefix, @@ -14,8 +14,8 @@ import { getEventLog, AlertUtils, ES_TEST_INDEX_NAME, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/health.ts similarity index 97% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/health.ts index d51cf8cc96af9..0663696dbee14 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/health.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/health.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getUrlPrefix, getTestRuleData, @@ -15,7 +15,7 @@ import { AlertUtils, ESTestIndexTool, ES_TEST_INDEX_NAME, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createFindTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/index.ts new file mode 100644 index 0000000000000..72890c2bbd90a --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/index.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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { setupSpacesAndUsers, tearDown } from '../../../setup'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { + describe('Alerts', () => { + describe('legacy alerts', function () { + before(async () => { + await setupSpacesAndUsers(getService); + }); + + after(async () => { + await tearDown(getService); + }); + + loadTestFile(require.resolve('./rbac_legacy')); + }); + + describe('alerts', () => { + before(async () => { + await setupSpacesAndUsers(getService); + }); + + after(async () => { + await tearDown(getService); + }); + + loadTestFile(require.resolve('./mute_all')); + loadTestFile(require.resolve('./mute_instance')); + loadTestFile(require.resolve('./unmute_all')); + loadTestFile(require.resolve('./unmute_instance')); + loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./update_api_key')); + loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./event_log')); + loadTestFile(require.resolve('./mustache_templates')); + loadTestFile(require.resolve('./health')); + loadTestFile(require.resolve('./excluded')); + loadTestFile(require.resolve('./snooze')); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mustache_templates.ts similarity index 92% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mustache_templates.ts index 7e3a7599a73e0..2426c154aa443 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mustache_templates.ts @@ -18,11 +18,11 @@ import axios from 'axios'; import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; -import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { getSlackServer } from '../../../common/fixtures/plugins/actions_simulators/server/plugin'; -import { getHttpProxyServer } from '../../../common/lib/get_proxy_server'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; // eslint-disable-next-line import/no-default-export export default function executionStatusAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts index 1cac93cb52b78..5a4c792463b62 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_all.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createMuteAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_instance.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_instance.ts index 3948f910423a9..63a285e0f4cb8 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/mute_instance.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createMuteAlertInstanceTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/rbac_legacy.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/rbac_legacy.ts index 4f324cae689cd..4f0f53383e206 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/rbac_legacy.ts @@ -7,10 +7,10 @@ import expect from '@kbn/expect'; import { SavedObjectsUtils } from '@kbn/core/server/saved_objects'; -import { UserAtSpaceScenarios, Superuser } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { ESTestIndexTool, getUrlPrefix, ObjectRemover, AlertUtils } from '../../../common/lib'; -import { setupSpacesAndUsers } from '..'; +import { UserAtSpaceScenarios, Superuser } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ESTestIndexTool, getUrlPrefix, ObjectRemover, AlertUtils } from '../../../../common/lib'; +import { setupSpacesAndUsers } from '../../../setup'; // eslint-disable-next-line import/no-default-export export default function alertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/snooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/snooze.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts index 929b95535e195..553e090498f00 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/snooze.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/snooze.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; const FUTURE_SNOOZE_TIME = '9999-12-31T06:00:00.000Z'; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts index e97e7e73abe44..dde198f54f771 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_all.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createUnmuteAlertTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_instance.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_instance.ts index 17ee25e822a6d..1aa84f64a7e79 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unmute_instance.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createMuteAlertInstanceTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unsnooze.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unsnooze.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts index ed37a19d80707..c868654235c21 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unsnooze.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/unsnooze.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createUnsnoozeRuleTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts index b2a1ae223f62c..c49fa62c606b6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { Response as SupertestResponse } from 'supertest'; -import { UserAtSpaceScenarios } from '../../scenarios'; +import { UserAtSpaceScenarios } from '../../../scenarios'; import { checkAAD, getUrlPrefix, @@ -16,8 +16,8 @@ import { ensureDatetimeIsWithinRange, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createUpdateTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update_api_key.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update_api_key.ts index 1c25ec550c41e..a0d1eb4dd0756 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update_api_key.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios } from '../../scenarios'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AlertUtils, checkAAD, @@ -16,7 +16,7 @@ import { ObjectRemover, getConsumerUnauthorizedErrorMessage, getProducerUnauthorizedErrorMessage, -} from '../../../common/lib'; +} from '../../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function createUpdateApiKeyTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/index.ts new file mode 100644 index 0000000000000..c4b5ab80c3416 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('alerting api integration security and spaces enabled - Group 2', function () { + loadTestFile(require.resolve('./telemetry')); + loadTestFile(require.resolve('./actions')); + loadTestFile(require.resolve('./alerting')); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/actions_telemetry.ts similarity index 98% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/actions_telemetry.ts index 1d8d0091b67de..b187b9e9f9759 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/actions_telemetry.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/actions_telemetry.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { Spaces, Superuser } from '../../scenarios'; +import { Spaces, Superuser } from '../../../scenarios'; import { getUrlPrefix, getEventLog, getTestRuleData, ObjectRemover, TaskManagerDoc, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createActionsTelemetryTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_telemetry.ts similarity index 99% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_telemetry.ts index afc39bf1c6b74..811eeecfc0375 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/alerting_telemetry.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_telemetry.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { Spaces, Superuser } from '../../scenarios'; +import { Spaces, Superuser } from '../../../scenarios'; import { getUrlPrefix, getEventLog, @@ -14,8 +14,8 @@ import { ObjectRemover, TaskManagerDoc, ESTestIndexTool, -} from '../../../common/lib'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +} from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createAlertingTelemetryTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/config.ts new file mode 100644 index 0000000000000..506d0016af4f6 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/config.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 { createTestConfig } from '../../../../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { + disabledPlugins: [], + license: 'trial', + ssl: true, + enableActionsProxy: true, + publicBaseUrl: true, + testFiles: [require.resolve('.')], +}); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/index.ts similarity index 84% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/index.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/index.ts index 9e73fafc9f7bd..6077997b661ae 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/telemetry/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { setupSpacesAndUsers, tearDown } from '..'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { setupSpacesAndUsers, tearDown } from '../../../setup'; // eslint-disable-next-line import/no-default-export export default function actionsTests({ loadTestFile, getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/setup.ts similarity index 73% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/setup.ts index 1196e3716347d..69ca58e1edc12 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/setup.ts @@ -6,8 +6,8 @@ */ import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { isCustomRoleSpecification } from '../../common/types'; -import { Spaces, Users } from '../scenarios'; +import { isCustomRoleSpecification } from '../common/types'; +import { Spaces, Users } from './scenarios'; export async function setupSpacesAndUsers(getService: FtrProviderContext['getService']) { const securityService = getService('security'); @@ -54,18 +54,3 @@ export async function tearDown(getService: FtrProviderContext['getService']) { await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); } - -// eslint-disable-next-line import/no-default-export -export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { - describe('alerting api integration security and spaces enabled', function () { - describe('', function () { - this.tags('ciGroup17'); - loadTestFile(require.resolve('./telemetry')); - loadTestFile(require.resolve('./actions')); - }); - - describe('', function () { - loadTestFile(require.resolve('./alerting')); - }); - }); -} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts deleted file mode 100644 index 9dd7326aa4663..0000000000000 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { setupSpacesAndUsers, tearDown } from '..'; - -// eslint-disable-next-line import/no-default-export -export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { - describe('Alerts', () => { - describe('legacy alerts', function () { - this.tags('ciGroup17'); - - before(async () => { - await setupSpacesAndUsers(getService); - }); - - after(async () => { - await tearDown(getService); - }); - - loadTestFile(require.resolve('./rbac_legacy')); - }); - - describe('alerts', () => { - before(async () => { - await setupSpacesAndUsers(getService); - }); - - after(async () => { - await tearDown(getService); - }); - - describe('', function () { - this.tags('ciGroup17'); - loadTestFile(require.resolve('./find')); - loadTestFile(require.resolve('./create')); - loadTestFile(require.resolve('./delete')); - loadTestFile(require.resolve('./disable')); - loadTestFile(require.resolve('./enable')); - loadTestFile(require.resolve('./execution_status')); - loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./get_alert_state')); - loadTestFile(require.resolve('./get_alert_summary')); - loadTestFile(require.resolve('./rule_types')); - }); - - describe('', function () { - this.tags('ciGroup30'); - loadTestFile(require.resolve('./mute_all')); - loadTestFile(require.resolve('./mute_instance')); - loadTestFile(require.resolve('./unmute_all')); - loadTestFile(require.resolve('./unmute_instance')); - loadTestFile(require.resolve('./update')); - loadTestFile(require.resolve('./update_api_key')); - loadTestFile(require.resolve('./alerts')); - loadTestFile(require.resolve('./event_log')); - loadTestFile(require.resolve('./mustache_templates')); - loadTestFile(require.resolve('./health')); - loadTestFile(require.resolve('./excluded')); - loadTestFile(require.resolve('./snooze')); - }); - }); - }); -} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index 24c6427fdf2f6..c6330e660aa24 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -131,7 +131,7 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body).to.eql({ connector_id: createdAction.id, status: 'error', - message: 'an error occurred while running the action executor', + message: 'an error occurred while running the action', service_message: `expected failure for ${ES_TEST_INDEX_NAME} ${reference}`, retry: false, }); @@ -142,7 +142,7 @@ export default function ({ getService }: FtrProviderContext) { actionTypeId: 'test.failing', outcome: 'failure', message: `action execution failure: test.failing:${createdAction.id}: failing action`, - errorMessage: `an error occurred while running the action executor: expected failure for .kibana-alerting-test-data actions-failure-1:space1`, + errorMessage: `an error occurred while running the action: expected failure for .kibana-alerting-test-data actions-failure-1:space1`, }); }); @@ -325,7 +325,7 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body).to.eql({ actionId: createdAction.id, status: 'error', - message: 'an error occurred while running the action executor', + message: 'an error occurred while running the action', serviceMessage: `expected failure for ${ES_TEST_INDEX_NAME} ${reference}`, retry: false, }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts index 588e7132f268c..4424175e36953 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts @@ -44,6 +44,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) rule_snoozed_status: { snoozed: 0, }, + rule_tags: [], }); }); @@ -122,6 +123,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) rule_snoozed_status: { snoozed: 0, }, + rule_tags: ['foo'], }); }); @@ -137,6 +139,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) { rule_type_id: 'test.noop', schedule: { interval: '1s' }, + tags: ['a', 'b'], }, 'ok' ); @@ -153,6 +156,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) params: { pattern: { instance: new Array(100).fill(true) }, }, + tags: ['a', 'c', 'f'], }, 'active' ); @@ -166,6 +170,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) { rule_type_id: 'test.throw', schedule: { interval: '1s' }, + tags: ['b', 'c', 'd'], }, 'error' ); @@ -202,6 +207,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) ruleSnoozedStatus: { snoozed: 0, }, + ruleTags: ['a', 'b', 'c', 'd', 'f'], }); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts index 5e9387ad4f0f9..6ae75c71d3bcf 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_execution_log.ts @@ -419,7 +419,7 @@ export default function createGetExecutionLogTests({ getService }: FtrProviderCo for (const errors of response.body.errors) { expect(errors.type).to.equal('actions'); expect(errors.message).to.equal( - `action execution failure: test.throw:${createdConnector.id}: connector that throws - an error occurred while running the action executor: this action is intended to fail` + `action execution failure: test.throw:${createdConnector.id}: connector that throws - an error occurred while running the action: this action is intended to fail` ); } }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts index 4622e84081506..999b0a5f4c4f5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -419,6 +419,18 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); }); + it('8.2.0 migrates existing esQuery alerts to contain searchType param', async () => { + const response = await es.get<{ alert: RawRule }>( + { + index: '.kibana', + id: 'alert:776cb5c0-ad1e-11ec-ab9e-5f5932f4fad8', + }, + { meta: true } + ); + expect(response.statusCode).to.equal(200); + expect(response.body._source?.alert?.params.searchType).to.eql('esQuery'); + }); + it('8.3.0 removes internal tags in Security Solution rule', async () => { const response = await es.get<{ alert: RawRule }>( { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts index 88e5e0740789f..424deaaf83815 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts @@ -11,8 +11,6 @@ import { Spaces } from '../scenarios'; // eslint-disable-next-line import/no-default-export export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('alerting api integration spaces only', function () { - this.tags('ciGroup12'); - loadTestFile(require.resolve('./actions')); loadTestFile(require.resolve('./alerting')); loadTestFile(require.resolve('./action_task_params')); diff --git a/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts b/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts index bb581a7f61cdb..757f2793b239f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts @@ -11,8 +11,6 @@ import { Spaces } from '../scenarios'; // eslint-disable-next-line import/no-default-export export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('alerting api integration spaces only legacy configuration', function () { - this.tags('ciGroup12'); - loadTestFile(require.resolve('./actions/builtin_action_types/webhook')); }); } diff --git a/x-pack/test/api_integration/apis/index.ts b/x-pack/test/api_integration/apis/index.ts index 1fa707eb10a98..0eb8556671038 100644 --- a/x-pack/test/api_integration/apis/index.ts +++ b/x-pack/test/api_integration/apis/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('apis', function () { - this.tags('ciGroup18'); - loadTestFile(require.resolve('./search')); loadTestFile(require.resolve('./es')); loadTestFile(require.resolve('./security')); diff --git a/x-pack/test/api_integration/apis/security/index.ts b/x-pack/test/api_integration/apis/security/index.ts index eb81d8245dbff..b595894f157fc 100644 --- a/x-pack/test/api_integration/apis/security/index.ts +++ b/x-pack/test/api_integration/apis/security/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security', function () { - this.tags('ciGroup18'); - // Updates here should be mirrored in `./security_basic.ts` if tests // should also run under a basic license. diff --git a/x-pack/test/api_integration/apis/security/security_basic.ts b/x-pack/test/api_integration/apis/security/security_basic.ts index 3dcb347126a97..1e7d8ed1ad4b2 100644 --- a/x-pack/test/api_integration/apis/security/security_basic.ts +++ b/x-pack/test/api_integration/apis/security/security_basic.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security (basic license)', function () { - this.tags('ciGroup6'); - // Updates here should be mirrored in `./index.js` if tests // should also run under a trial/platinum license. diff --git a/x-pack/test/api_integration/apis/security/security_trial.ts b/x-pack/test/api_integration/apis/security/security_trial.ts index be4ad252d408f..3786240b4e433 100644 --- a/x-pack/test/api_integration/apis/security/security_trial.ts +++ b/x-pack/test/api_integration/apis/security/security_trial.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security (trial license)', function () { - this.tags('ciGroup6'); - // THIS TEST NEEDS TO BE LAST. IT IS DESTRUCTIVE! IT REMOVES TRIAL LICENSE!!! loadTestFile(require.resolve('./license_downgrade')); }); diff --git a/x-pack/test/api_integration/apis/spaces/index.ts b/x-pack/test/api_integration/apis/spaces/index.ts index 3ca0040e39ec9..1ec1faac535ce 100644 --- a/x-pack/test/api_integration/apis/spaces/index.ts +++ b/x-pack/test/api_integration/apis/spaces/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('spaces', function () { - this.tags('ciGroup18'); - loadTestFile(require.resolve('./get_active_space')); loadTestFile(require.resolve('./saved_objects')); loadTestFile(require.resolve('./space_attributes')); diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index 4e47947aa29dc..8cc5fb6f57d42 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -10,7 +10,7 @@ import { services } from './services'; export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProviderContext) { const xPackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') + require.resolve('../functional/config.base.js') ); return { diff --git a/x-pack/test/api_integration_basic/apis/index.ts b/x-pack/test/api_integration_basic/apis/index.ts index 9490d4c277675..31176c55ac3ca 100644 --- a/x-pack/test/api_integration_basic/apis/index.ts +++ b/x-pack/test/api_integration_basic/apis/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('apis', function () { - this.tags('ciGroup11'); - loadTestFile(require.resolve('./transform')); loadTestFile(require.resolve('./security_solution')); }); diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 59764cf021c4a..e1312f589862c 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -14,8 +14,6 @@ export default function apmApiIntegrationTests({ getService, loadTestFile }: Ftr const registry = getService('registry'); describe('APM API tests', function () { - this.tags('ciGroup1'); - const tests = glob.sync('**/*.spec.ts', { cwd }); tests.forEach((test) => { describe(test, function () { diff --git a/x-pack/test/banners_functional/config.ts b/x-pack/test/banners_functional/config.ts index 03f91dfbc34e2..83d0c4656572c 100644 --- a/x-pack/test/banners_functional/config.ts +++ b/x-pack/test/banners_functional/config.ts @@ -9,7 +9,9 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { services, pageObjects } from './ftr_provider_context'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const kibanaFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const kibanaFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); return { testFiles: [require.resolve('./tests')], diff --git a/x-pack/test/banners_functional/tests/index.ts b/x-pack/test/banners_functional/tests/index.ts index 301c872c746e1..8a26cb66ad569 100644 --- a/x-pack/test/banners_functional/tests/index.ts +++ b/x-pack/test/banners_functional/tests/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('banners - functional tests', function () { - this.tags('ciGroup2'); - loadTestFile(require.resolve('./global')); loadTestFile(require.resolve('./spaces')); }); diff --git a/x-pack/test/cases_api_integration/common/lib/mock.ts b/x-pack/test/cases_api_integration/common/lib/mock.ts index 34f1d4a9273d2..08f30f8df024e 100644 --- a/x-pack/test/cases_api_integration/common/lib/mock.ts +++ b/x-pack/test/cases_api_integration/common/lib/mock.ts @@ -17,6 +17,7 @@ import { CaseStatuses, CommentRequest, CommentRequestActionsType, + CaseSeverity, } from '@kbn/cases-plugin/common/api'; export const defaultUser = { email: null, full_name: null, username: 'elastic' }; @@ -86,6 +87,7 @@ export const postCaseResp = ( ...(id != null ? { id } : {}), comments: [], duration: null, + severity: req.severity ?? CaseSeverity.LOW, totalAlerts: 0, totalComment: 0, closed_by: null, diff --git a/x-pack/test/cases_api_integration/common/lib/utils.ts b/x-pack/test/cases_api_integration/common/lib/utils.ts index bb8f31abefd47..825f50b792cc6 100644 --- a/x-pack/test/cases_api_integration/common/lib/utils.ts +++ b/x-pack/test/cases_api_integration/common/lib/utils.ts @@ -1010,13 +1010,13 @@ export const getCaseMetrics = async ({ }: { supertest: SuperTest.SuperTest; caseId: string; - features: string[]; + features: string[] | string; expectedHttpCode?: number; auth?: { user: User; space: string | null }; }): Promise => { const { body: metricsResponse } = await supertest .get(`${getSpaceUrlPrefix(auth?.space)}${CASES_URL}/metrics/${caseId}`) - .query({ features: JSON.stringify(features) }) + .query({ features }) .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode); @@ -1277,14 +1277,14 @@ export const getCasesMetrics = async ({ auth = { user: superUser, space: null }, }: { supertest: SuperTest.SuperTest; - features: string[]; + features: string[] | string; query?: Record; expectedHttpCode?: number; auth?: { user: User; space: string | null }; }): Promise => { const { body: metricsResponse } = await supertest .get(`${getSpaceUrlPrefix(auth?.space)}${CASES_URL}/metrics`) - .query({ features: JSON.stringify(features), ...query }) + .query({ features, ...query }) .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/index.ts index b618cf5b4df68..714d7e0c6b219 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/basic/index.ts @@ -11,9 +11,6 @@ import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/ // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases security and spaces enabled: basic', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup27'); - before(async () => { await createSpacesAndUsers(getService); }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts index 44da07a845ff7..2ce441c37e687 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts @@ -27,6 +27,7 @@ import { CommentUserAction, CreateCaseUserAction, CaseStatuses, + CaseSeverity, } from '@kbn/cases-plugin/common/api'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { @@ -204,6 +205,10 @@ const expectExportToHaveCaseSavedObject = ( 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); + expect(createdCaseSO.attributes.status).to.eql(CaseStatuses.open); + expect(createdCaseSO.attributes.severity).to.eql(CaseSeverity.LOW); + expect(createdCaseSO.attributes.duration).to.eql(null); + expect(createdCaseSO.attributes.tags).to.eql(caseRequest.tags); }; const expectExportToHaveUserActions = (objects: SavedObject[], caseRequest: CasePostRequest) => { @@ -239,6 +244,7 @@ const expectCaseCreateUserAction = ( expect(restParsedCreateCase).to.eql({ ...restCreateCase, status: CaseStatuses.open, + severity: CaseSeverity.LOW, }); expect(restParsedConnector).to.eql(restConnector); }; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts index 3c43ac1932986..4d4b9d45b6717 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts @@ -369,7 +369,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); }); - describe('8.3.0 adding duration', () => { + describe('8.3.0', () => { before(async () => { await kibanaServer.importExport.load( 'x-pack/test/functional/fixtures/kbn_archiver/cases/8.2.0/cases_duration.json' @@ -383,34 +383,48 @@ export default function createGetTests({ getService }: FtrProviderContext) { await deleteAllCaseItems(es); }); - it('calculates the correct duration for closed cases', async () => { - const caseInfo = await getCase({ - supertest, - caseId: '4537b380-a512-11ec-b92f-859b9e89e434', + describe('adding duration', () => { + it('calculates the correct duration for closed cases', async () => { + const caseInfo = await getCase({ + supertest, + caseId: '4537b380-a512-11ec-b92f-859b9e89e434', + }); + + expect(caseInfo).to.have.property('duration'); + expect(caseInfo.duration).to.be(120); }); - expect(caseInfo).to.have.property('duration'); - expect(caseInfo.duration).to.be(120); - }); + it('sets the duration to null to open cases', async () => { + const caseInfo = await getCase({ + supertest, + caseId: '7537b580-a512-11ec-b94f-85979e89e434', + }); - it('sets the duration to null to open cases', async () => { - const caseInfo = await getCase({ - supertest, - caseId: '7537b580-a512-11ec-b94f-85979e89e434', + expect(caseInfo).to.have.property('duration'); + expect(caseInfo.duration).to.be(null); }); - expect(caseInfo).to.have.property('duration'); - expect(caseInfo.duration).to.be(null); - }); + it('sets the duration to null to in-progress cases', async () => { + const caseInfo = await getCase({ + supertest, + caseId: '1537b580-a512-11ec-b94f-85979e89e434', + }); - it('sets the duration to null to in-progress cases', async () => { - const caseInfo = await getCase({ - supertest, - caseId: '1537b580-a512-11ec-b94f-85979e89e434', + expect(caseInfo).to.have.property('duration'); + expect(caseInfo.duration).to.be(null); }); + }); - expect(caseInfo).to.have.property('duration'); - expect(caseInfo.duration).to.be(null); + describe('add severity', () => { + it('adds the severity field for existing documents', async () => { + const caseInfo = await getCase({ + supertest, + caseId: '4537b380-a512-11ec-b92f-859b9e89e434', + }); + + expect(caseInfo).to.have.property('severity'); + expect(caseInfo.severity).to.be('low'); + }); }); }); }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 9ef1c3d5655e4..80dffef7cd3ee 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -10,6 +10,7 @@ import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '@kbn/security-solution-plugin/common/constants'; import { + CaseSeverity, CasesResponse, CaseStatuses, CommentType, @@ -170,6 +171,34 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('should patch the severity of a case correctly', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + // the default severity + expect(postedCase.severity).equal(CaseSeverity.LOW); + + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + severity: CaseSeverity.MEDIUM, + }, + ], + }, + }); + + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + + expect(data).to.eql({ + ...postCaseResp(), + severity: CaseSeverity.MEDIUM, + updated_by: defaultUser, + }); + }); + it('should patch a case with new connector', async () => { const postedCase = await createCase(supertest, postCaseReq); const patchedCases = await updateCase({ @@ -297,6 +326,22 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('400s when a wrong severity value is passed', async () => { + await updateCase({ + supertest, + params: { + cases: [ + { + version: 'version', + // @ts-expect-error + severity: 'wont-do', + }, + ], + }, + expectedHttpCode: 400, + }); + }); + it('400s when id is missing', async () => { await updateCase({ supertest, diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts index 9cd986a032b24..d4b52ff6f3394 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -12,6 +12,7 @@ import { ConnectorTypes, ConnectorJiraTypeFields, CaseStatuses, + CaseSeverity, } from '@kbn/cases-plugin/common/api'; import { getPostCaseRequest, postCaseResp, defaultUser } from '../../../../common/lib/mock'; import { @@ -102,6 +103,32 @@ export default ({ getService }: FtrProviderContext): void => { ); }); + it('should post a case without severity', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql(postCaseResp(null, getPostCaseRequest())); + }); + + it('should post a case with severity', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + severity: CaseSeverity.HIGH, + }) + ); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql( + postCaseResp( + null, + getPostCaseRequest({ + severity: CaseSeverity.HIGH, + }) + ) + ); + }); + it('should create a user action when creating a case', async () => { const postedCase = await createCase(supertest, getPostCaseRequest()); const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); @@ -122,6 +149,7 @@ export default ({ getService }: FtrProviderContext): void => { settings: postedCase.settings, owner: postedCase.owner, status: CaseStatuses.open, + severity: CaseSeverity.LOW, }, }); }); @@ -207,6 +235,11 @@ export default ({ getService }: FtrProviderContext): void => { await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(caseWithoutTags).expect(400); }); + it('400s when passing a wrong severity value', async () => { + // @ts-expect-error + await createCase(supertest, { ...getPostCaseRequest(), severity: 'very-severe' }, 400); + }); + it('400s if you passing status for a new case', async () => { const req = getPostCaseRequest(); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics.ts index cf1b842db0fe7..d08a00ab4f088 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics.ts @@ -30,6 +30,24 @@ export default ({ getService }: FtrProviderContext): void => { const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('case metrics', () => { + it('accepts the features as string', async () => { + const newCase = await createCase(supertest, getPostCaseRequest()); + + const metrics = await getCaseMetrics({ + supertest, + caseId: newCase.id, + features: 'connectors', + }); + + expect(metrics).to.eql({ + connectors: { + total: 0, + }, + }); + + await deleteAllCaseItems(es); + }); + describe('closed case from kbn archive', () => { const closedCaseId = 'e49ad6e0-cf9d-11eb-a603-13e7747d215z'; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_cases_metrics.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_cases_metrics.ts index c1abfada39dd2..e3e0c2cb20570 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_cases_metrics.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_cases_metrics.ts @@ -34,6 +34,17 @@ export default ({ getService }: FtrProviderContext): void => { const kibanaServer = getService('kibanaServer'); describe('all cases metrics', () => { + it('accepts the features as string', async () => { + const metrics = await getCasesMetrics({ + supertest, + features: 'mttr', + }); + + expect(metrics).to.eql({ mttr: 0 }); + + await deleteAllCaseItems(es); + }); + describe('MTTR', () => { it('responses with zero if there are no cases', async () => { const metrics = await getCasesMetrics({ diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 283e6b0c5301b..aacb5f6c8ae17 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; import { CaseResponse, + CaseSeverity, CaseStatuses, CommentType, ConnectorTypes, @@ -106,6 +107,30 @@ export default ({ getService }: FtrProviderContext): void => { expect(statusUserAction.payload).to.eql({ status: 'closed' }); }); + it('creates a severity update user action when changing the severity', async () => { + const theCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: theCase.id, + version: theCase.version, + severity: CaseSeverity.HIGH, + }, + ], + }, + }); + + const userActions = await getCaseUserActions({ supertest, caseID: theCase.id }); + const statusUserAction = userActions[1]; + + expect(userActions.length).to.eql(2); + expect(statusUserAction.type).to.eql('severity'); + expect(statusUserAction.action).to.eql('update'); + expect(statusUserAction.payload).to.eql({ severity: 'high' }); + }); + it('creates a connector update user action', async () => { const newConnector = { id: '123', diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts index 3c1ee84296270..e768c92d8c8af 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts @@ -19,23 +19,15 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { await deleteSpacesAndUsers(getService); }); - describe('', function () { - this.tags('ciGroup13'); + // Trial + loadTestFile(require.resolve('./cases/push_case')); + loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); + loadTestFile(require.resolve('./configure')); - // Trial - loadTestFile(require.resolve('./cases/push_case')); - loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); - loadTestFile(require.resolve('./configure')); - }); - - describe('', function () { - this.tags('ciGroup25'); + // Common + loadTestFile(require.resolve('../common')); - // Common - loadTestFile(require.resolve('../common')); - - // NOTE: These need to be at the end because they could delete the .kibana index and inadvertently remove the users and spaces - loadTestFile(require.resolve('../common/migrations')); - }); + // NOTE: These need to be at the end because they could delete the .kibana index and inadvertently remove the users and spaces + loadTestFile(require.resolve('../common/migrations')); }); }; diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts index 346640aa6b9de..cbf0f010048ef 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/index.ts @@ -11,9 +11,6 @@ import { createSpaces, deleteSpaces } from '../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases spaces only enabled: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - before(async () => { await createSpaces(getService); }); diff --git a/x-pack/test/cloud_integration/config.ts b/x-pack/test/cloud_integration/config.ts index 102d276b34584..0074565875845 100644 --- a/x-pack/test/cloud_integration/config.ts +++ b/x-pack/test/cloud_integration/config.ts @@ -25,7 +25,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); const kibanaPort = kibanaFunctionalConfig.get('servers.kibana.port'); diff --git a/x-pack/test/common/services/bsearch_secure.ts b/x-pack/test/common/services/bsearch_secure.ts index 458e0878e187f..f60dc6aa4c3cd 100644 --- a/x-pack/test/common/services/bsearch_secure.ts +++ b/x-pack/test/common/services/bsearch_secure.ts @@ -79,12 +79,11 @@ export const BSecureSearchFactory = (retry: RetryService) => ({ .set('kbn-xsrf', 'true') .send(options); } - if (result.status === 500 || result.status === 200) { + if ((result.status === 500 || result.status === 200) && result.body) { return result; } throw new Error('try again'); }); - if (body.isRunning) { const result = await retry.try(async () => { const resp = await supertestWithoutAuth diff --git a/x-pack/test/detection_engine_api_integration/basic/config.ts b/x-pack/test/detection_engine_api_integration/basic/config.ts index 0c5c7d1649f84..26fdc62e0ec52 100644 --- a/x-pack/test/detection_engine_api_integration/basic/config.ts +++ b/x-pack/test/detection_engine_api_integration/basic/config.ts @@ -8,7 +8,10 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export -export default createTestConfig('basic', { - license: 'basic', - ssl: true, -}); +export default createTestConfig( + { + license: 'basic', + ssl: true, + }, + [require.resolve('./tests')] +); diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts index 1a5ea8de935b4..349d33c89fdf3 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('detection engine api basic license', function () { - this.tags('ciGroup1'); - loadTestFile(require.resolve('./add_prepackaged_rules')); loadTestFile(require.resolve('./create_rules')); loadTestFile(require.resolve('./create_rules_bulk')); diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index 96a83d2c3a427..a95cb937d4cd9 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -31,7 +31,7 @@ const enabledActionTypes = [ 'test.rate-limit', ]; -export function createTestConfig(name: string, options: CreateTestConfigOptions) { +export function createTestConfig(options: CreateTestConfigOptions, testFiles?: string[]) { const { license = 'trial', ssl = false } = options; return async ({ readConfigFile }: FtrConfigProviderContext) => { @@ -47,7 +47,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) }; return { - testFiles: [require.resolve(`../${name}/tests/`)], + testFiles, servers, services, junit: { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/README.md b/x-pack/test/detection_engine_api_integration/security_and_spaces/README.md new file mode 100644 index 0000000000000..ca10827803c65 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/README.md @@ -0,0 +1,33 @@ +# What are all these groups? + +These tests take a while so they have been broken up into groups with their own `config.ts` and `index.ts` file, causing each of these groups to be independent bundles of tests which can be run on some worker in CI without taking an incredible amount of time. + +Want to change the groups to something more logical? Have fun! Just make sure that each group executes on CI in less than 10 minutes or so. We don't currently have any mechanism for validating this right now, you just need to look at the times in the log output on CI, but we'll be working on tooling for making this information more accessible soon. + +- Kibana Operations + +# Rule Exception List Tests + +These tests are currently in group7-9. + +These are tests for rule exception lists where we test each data type + +- date +- double +- float +- integer +- ip +- keyword +- long +- text + +Against the operator types of: + +- "is" +- "is not" +- "is one of" +- "is not one of" +- "exists" +- "does not exist" +- "is in list" +- "is not in list" diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/config.base.ts similarity index 87% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/config.base.ts index 78203525b887a..ed72e1faa682c 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/config.base.ts @@ -8,7 +8,7 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export -export default createTestConfig('security_and_spaces', { +export default createTestConfig({ license: 'trial', ssl: true, }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_actions.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_actions.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_prepackaged_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/add_prepackaged_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/aliases.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/aliases.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/aliases.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/check_privileges.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/check_privileges.ts diff --git a/x-pack/test/functional_embedded/config.firefox.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/config.ts similarity index 54% rename from x-pack/test/functional_embedded/config.firefox.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/config.ts index 49359d37673de..2430b8f2148d9 100644 --- a/x-pack/test/functional_embedded/config.firefox.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/config.ts @@ -7,23 +7,12 @@ import { FtrConfigProviderContext } from '@kbn/test'; +// eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const chromeConfig = await readConfigFile(require.resolve('./config')); + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); return { - ...chromeConfig.getAll(), - - browser: { - type: 'firefox', - acceptInsecureCerts: true, - }, - - suiteTags: { - exclude: ['skipFirefox'], - }, - - junit: { - reportName: 'Firefox Kibana Embedded in iframe with X-Pack Security', - }, + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], }; } diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_index.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_index.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_ml.ts similarity index 98% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_ml.ts index 20c914e17ce70..94d65933bc9dc 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_ml.ts @@ -92,7 +92,8 @@ export default ({ getService }: FtrProviderContext) => { return body; } - describe('Generating signals from ml anomalies', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/125033 + describe.skip('Generating signals from ml anomalies', () => { before(async () => { // Order is critical here: auditbeat data must be loaded before attempting to start the ML job, // as the job looks for certain indices on start diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_signals_migrations.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_signals_migrations.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_signals_migrations.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_threat_matching.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_threat_matching.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts similarity index 99% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts index 69cbd9fd4d4c7..f138bad4b9d2c 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts @@ -20,6 +20,7 @@ import { getSimpleRuleOutput, getSimpleRuleOutputWithoutRuleId, getSimpleRuleWithoutRuleId, + getSlackAction, getWebHookAction, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, @@ -148,7 +149,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: hookAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') - .send(getWebHookAction()) + .send(getSlackAction()) .expect(200); // create a rule without actions diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts similarity index 99% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts index 29f5d5edf49d9..d05dcb690994c 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts @@ -20,6 +20,7 @@ import { getSimpleRuleOutput, getSimpleRuleOutputWithoutRuleId, getSimpleRuleWithoutRuleId, + getSlackAction, getWebHookAction, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, @@ -277,7 +278,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: hookAction } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') - .send(getWebHookAction()) + .send(getSlackAction()) .expect(200); // create a rule without actions @@ -318,12 +319,12 @@ export default ({ getService }: FtrProviderContext): void => { const { body: hookAction1 } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') - .send(getWebHookAction()) + .send(getSlackAction()) .expect(200); const { body: hookAction2 } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') - .send(getWebHookAction()) + .send(getSlackAction()) .expect(200); // create 2 rules without actions diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_signals_migrations.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_signals_migrations.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/export_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/finalize_signals_migrations.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/finalize_signals_migrations.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/generating_signals.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/generating_signals.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_prepackaged_rules_status.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_prepackaged_rules_status.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_prepackaged_rules_status.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_rule_execution_events.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_rule_execution_events.ts similarity index 62% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_rule_execution_events.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_rule_execution_events.ts index d3bddc8b0d46c..9fadbd39f7704 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_rule_execution_events.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_rule_execution_events.ts @@ -52,11 +52,6 @@ export default ({ getService }: FtrProviderContext) => { await deleteAllEventLogExecutionEvents(es, log); }); - afterEach(async () => { - await deleteAllAlerts(supertest, log); - await deleteAllEventLogExecutionEvents(es, log); - }); - it('should return an error if rule does not exist', async () => { const start = dateMath.parse('now-24h')?.utc().toISOString(); const end = dateMath.parse('now', { roundUp: true })?.utc().toISOString(); @@ -90,7 +85,7 @@ export default ({ getService }: FtrProviderContext) => { expect(response.body.events[0].search_duration_ms).to.greaterThan(0); expect(response.body.events[0].schedule_delay_ms).to.greaterThan(0); expect(response.body.events[0].indexing_duration_ms).to.greaterThan(0); - expect(response.body.events[0].gap_duration_ms).to.eql(0); + expect(response.body.events[0].gap_duration_s).to.eql(0); expect(response.body.events[0].security_status).to.eql('succeeded'); expect(response.body.events[0].security_message).to.eql('succeeded'); }); @@ -114,7 +109,7 @@ export default ({ getService }: FtrProviderContext) => { expect(response.body.events[0].search_duration_ms).to.eql(0); expect(response.body.events[0].schedule_delay_ms).to.greaterThan(0); expect(response.body.events[0].indexing_duration_ms).to.eql(0); - expect(response.body.events[0].gap_duration_ms).to.eql(0); + expect(response.body.events[0].gap_duration_s).to.eql(0); expect(response.body.events[0].security_status).to.eql('partial failure'); expect( response.body.events[0].security_message.startsWith( @@ -123,16 +118,15 @@ export default ({ getService }: FtrProviderContext) => { ).to.eql(true); }); - // TODO: Debug indexing - it.skip('should return execution events for a rule that has executed in a failure state with a gap', async () => { + it('should return execution events for a rule that has executed in a failure state with a gap', async () => { const rule = getRuleForSignalTesting(['auditbeat-*'], uuid.v4(), false); const { id } = await createRule(supertest, log, rule); const start = dateMath.parse('now')?.utc().toISOString(); const end = dateMath.parse('now+24h', { roundUp: true })?.utc().toISOString(); - // Create 5 timestamps a minute apart to use in the templated data - const dateTimes = [...Array(5).keys()].map((i) => + // Create 5 timestamps (failedGapExecution.length) a minute apart to use in the templated data + const dateTimes = [...Array(failedGapExecution.length).keys()].map((i) => moment(start) .add(i + 1, 'm') .toDate() @@ -144,6 +138,7 @@ export default ({ getService }: FtrProviderContext) => { set(e, 'event.start', dateTimes[i]); set(e, 'event.end', dateTimes[i]); set(e, 'rule.id', id); + set(e, 'kibana.saved_objects[0].id', id); return e; }); @@ -155,73 +150,19 @@ export default ({ getService }: FtrProviderContext) => { .set('kbn-xsrf', 'true') .query({ start, end }); - // console.log(JSON.stringify(response)); - expect(response.status).to.eql(200); expect(response.body.total).to.eql(1); - expect(response.body.events[0].duration_ms).to.eql(4236); + expect(response.body.events[0].duration_ms).to.eql(1545); expect(response.body.events[0].search_duration_ms).to.eql(0); - expect(response.body.events[0].schedule_delay_ms).to.greaterThan(0); + expect(response.body.events[0].schedule_delay_ms).to.eql(544808); expect(response.body.events[0].indexing_duration_ms).to.eql(0); - expect(response.body.events[0].gap_duration_ms).to.greaterThan(0); + expect(response.body.events[0].gap_duration_s).to.eql(245); expect(response.body.events[0].security_status).to.eql('failed'); expect( response.body.events[0].security_message.startsWith( - 'Check privileges failed to execute ResponseError: index_not_found_exception: [index_not_found_exception] Reason: no such index [no-name-index]' + '4 minutes (244689ms) were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances.' ) ).to.eql(true); }); - - // it('should return execution events when providing a status filter', async () => { - // const rule = getRuleForSignalTesting(['auditbeat-*', 'no-name-index']); - // const { id } = await createRule(supertest, log, rule); - // await waitForRuleSuccessOrStatus(supertest, log, id, RuleExecutionStatus.failed); - // await waitForSignalsToBePresent(supertest, log, 1, [id]); - // - // const start = dateMath.parse('now-24h')?.utc().toISOString(); - // const end = dateMath.parse('now', { roundUp: true })?.utc().toISOString(); - // const response = await supertest - // .get(detectionEngineRuleExecutionEventsUrl(id)) - // .set('kbn-xsrf', 'true') - // .query({ start, end }); - // - // expect(response.status).to.eql(200); - // expect(response.body.total).to.eql(1); - // expect(response.body.events[0].duration_ms).to.greaterThan(0); - // expect(response.body.events[0].search_duration_ms).to.eql(0); - // expect(response.body.events[0].schedule_delay_ms).to.greaterThan(0); - // expect(response.body.events[0].indexing_duration_ms).to.eql(0); - // expect(response.body.events[0].gap_duration_ms).to.eql(0); - // expect(response.body.events[0].security_status).to.eql('failed'); - // expect(response.body.events[0].security_message).to.include( - // 'were not queried between this rule execution and the last execution, so signals may have been missed. ' - // ); - // }); - - // it('should return execution events when providing a status filter and sortField', async () => { - // const rule = getRuleForSignalTesting(['auditbeat-*', 'no-name-index']); - // const { id } = await createRule(supertest, log, rule); - // await waitForRuleSuccessOrStatus(supertest, log, id, RuleExecutionStatus.failed); - // await waitForSignalsToBePresent(supertest, log, 1, [id]); - // - // const start = dateMath.parse('now-24h')?.utc().toISOString(); - // const end = dateMath.parse('now', { roundUp: true })?.utc().toISOString(); - // const response = await supertest - // .get(detectionEngineRuleExecutionEventsUrl(id)) - // .set('kbn-xsrf', 'true') - // .query({ start, end }); - // - // expect(response.status).to.eql(200); - // expect(response.body.total).to.eql(1); - // expect(response.body.events[0].duration_ms).to.greaterThan(0); - // expect(response.body.events[0].search_duration_ms).to.eql(0); - // expect(response.body.events[0].schedule_delay_ms).to.greaterThan(0); - // expect(response.body.events[0].indexing_duration_ms).to.eql(0); - // expect(response.body.events[0].gap_duration_ms).to.eql(0); - // expect(response.body.events[0].security_status).to.eql('failed'); - // expect(response.body.events[0].security_message).to.include( - // 'were not queried between this rule execution and the last execution, so signals may have been missed. ' - // ); - // }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_signals_migration_status.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/get_signals_migration_status.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/ignore_fields.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/ignore_fields.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/ignore_fields.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/ignore_fields.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/import_export_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_export_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/import_export_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/import_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/import_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts new file mode 100644 index 0000000000000..f7a96c2f496d8 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.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. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 1', function () { + // !!NOTE: For new routes that do any updates on a rule, please ensure that you are including the legacy + // action migration code. We are monitoring legacy action telemetry to clean up once we see their + // existence being near 0. + + loadTestFile(require.resolve('./aliases')); + loadTestFile(require.resolve('./add_actions')); + loadTestFile(require.resolve('./update_actions')); + loadTestFile(require.resolve('./add_prepackaged_rules')); + loadTestFile(require.resolve('./check_privileges')); + loadTestFile(require.resolve('./create_index')); + loadTestFile(require.resolve('./create_rules')); + loadTestFile(require.resolve('./preview_rules')); + loadTestFile(require.resolve('./create_rules_bulk')); + loadTestFile(require.resolve('./create_ml')); + loadTestFile(require.resolve('./create_threat_matching')); + loadTestFile(require.resolve('./delete_rules')); + loadTestFile(require.resolve('./delete_rules_bulk')); + loadTestFile(require.resolve('./export_rules')); + loadTestFile(require.resolve('./find_rules')); + loadTestFile(require.resolve('./generating_signals')); + loadTestFile(require.resolve('./get_prepackaged_rules_status')); + loadTestFile(require.resolve('./get_rule_execution_events')); + loadTestFile(require.resolve('./import_rules')); + loadTestFile(require.resolve('./import_export_rules')); + loadTestFile(require.resolve('./legacy_actions_migrations')); + loadTestFile(require.resolve('./read_rules')); + loadTestFile(require.resolve('./resolve_read_rules')); + loadTestFile(require.resolve('./update_rules')); + loadTestFile(require.resolve('./update_rules_bulk')); + loadTestFile(require.resolve('./patch_rules_bulk')); + loadTestFile(require.resolve('./perform_bulk_action')); + loadTestFile(require.resolve('./patch_rules')); + loadTestFile(require.resolve('./read_privileges')); + loadTestFile(require.resolve('./open_close_signals')); + loadTestFile(require.resolve('./get_signals_migration_status')); + loadTestFile(require.resolve('./create_signals_migrations')); + loadTestFile(require.resolve('./finalize_signals_migrations')); + loadTestFile(require.resolve('./delete_signals_migrations')); + loadTestFile(require.resolve('./timestamps')); + loadTestFile(require.resolve('./runtime')); + loadTestFile(require.resolve('./throttle')); + loadTestFile(require.resolve('./ignore_fields')); + loadTestFile(require.resolve('./migrations')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/legacy_actions_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/legacy_actions_migrations.ts new file mode 100644 index 0000000000000..ad6e45954e06a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/legacy_actions_migrations.ts @@ -0,0 +1,322 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + getLegacyActionSOById, + getLegacyActionNotificationSOById, + getRuleSOById, +} from '../../utils'; + +/** + * @deprecated Once the legacy notification system is removed, remove this test too. + */ +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + // This test suite is not meant to test a specific route, but to test the legacy action migration + // code that lives in multiple routes. This code is also tested in each of the routes it lives in + // but not in as much detail and relying on mocks. This test loads an es_archive containing rules + // created in 7.15 with legacy actions. + // For new routes that do any updates on a rule, please ensure that you are including the legacy + // action migration code. We are monitoring legacy action telemetry to clean up once we see their + // existence being near 0. + describe('migrate_legacy_actions', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/legacy_actions'); + }); + + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/legacy_actions' + ); + }); + + it('migrates legacy actions for rule with no actions', async () => { + const soId = '9095ee90-b075-11ec-bb3f-1f063f8e06cf'; + const ruleId = '2297be91-894c-4831-830f-b424a0ec84f0'; + const legacySidecarId = '926668d0-b075-11ec-bb3f-1f063f8e06cf'; + + // check for legacy sidecar action + const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionSO.hits.hits.length).to.eql(1); + + // check for legacy notification SO + // should not have been created for a rule with no actions + const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); + expect(legacyNotificationSO.hits.hits.length).to.eql(0); + + // patch enable the rule + // any route that edits the rule should trigger the migration + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: ruleId, enabled: false }) + .expect(200); + + const { + hits: { + hits: [{ _source: ruleSO }], + }, + } = await getRuleSOById(es, soId); + + // Sidecar should be removed + const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); + + expect(ruleSO?.alert.actions).to.eql([]); + expect(ruleSO?.alert.throttle).to.eql(null); + expect(ruleSO?.alert.notifyWhen).to.eql('onActiveAlert'); + }); + + it('migrates legacy actions for rule with action run on every run', async () => { + const soId = 'dc6595f0-b075-11ec-bb3f-1f063f8e06cf'; + const ruleId = '72a0d429-363b-4f70-905e-c6019a224d40'; + const legacySidecarId = 'dde13970-b075-11ec-bb3f-1f063f8e06cf'; + + // check for legacy sidecar action + const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionSO.hits.hits.length).to.eql(1); + + // check for legacy notification SO + // should not have been created for a rule that runs on every rule run + const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); + expect(legacyNotificationSO.hits.hits.length).to.eql(0); + + // patch enable the rule + // any route that edits the rule should trigger the migration + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: ruleId, enabled: false }) + .expect(200); + + const { + hits: { + hits: [{ _source: ruleSO }], + }, + } = await getRuleSOById(es, soId); + + // Sidecar should be removed + const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); + + expect(ruleSO?.alert.actions).to.eql([ + { + actionRef: 'action_0', + actionTypeId: '.email', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + subject: 'Test Actions', + to: ['test@test.com'], + }, + }, + ]); + expect(ruleSO?.alert.throttle).to.eql(null); + expect(ruleSO?.alert.notifyWhen).to.eql('onActiveAlert'); + expect(ruleSO?.references).to.eql([ + { + id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', + name: 'action_0', + type: 'action', + }, + ]); + }); + + it('migrates legacy actions for rule with action run hourly', async () => { + const soId = '064e3160-b076-11ec-bb3f-1f063f8e06cf'; + const ruleId = '4c056b05-75ac-4209-be32-82100f771eb4'; + const legacySidecarId = '07aa8d10-b076-11ec-bb3f-1f063f8e06cf'; + + // check for legacy sidecar action + const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionSO.hits.hits.length).to.eql(1); + + // check for legacy notification SO + const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); + expect(legacyNotificationSO.hits.hits.length).to.eql(1); + + // patch enable the rule + // any route that edits the rule should trigger the migration + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: ruleId, enabled: false }) + .expect(200); + + const { + hits: { + hits: [{ _source: ruleSO }], + }, + } = await getRuleSOById(es, soId); + + // Sidecar should be removed + const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); + + // Legacy notification should be removed + const legacyNotificationSOAfterMigration = await getLegacyActionNotificationSOById(es, soId); + expect(legacyNotificationSOAfterMigration.hits.hits.length).to.eql(0); + + expect(ruleSO?.alert.actions).to.eql([ + { + actionTypeId: '.email', + params: { + subject: 'Rule email', + to: ['test@test.com'], + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + actionRef: 'action_0', + group: 'default', + }, + { + actionTypeId: '.slack', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + }, + actionRef: 'action_1', + group: 'default', + }, + ]); + expect(ruleSO?.alert.throttle).to.eql('1h'); + expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.references).to.eql([ + { + id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', + name: 'action_0', + type: 'action', + }, + { + id: '207fa0e0-c04e-11ec-8a52-4fb92379525a', + name: 'action_1', + type: 'action', + }, + ]); + }); + + it('migrates legacy actions for rule with action run daily', async () => { + const soId = '27639570-b076-11ec-bb3f-1f063f8e06cf'; + const ruleId = '8e2c8550-f13f-4e21-be0c-92148d71a5f1'; + const legacySidecarId = '291ae260-b076-11ec-bb3f-1f063f8e06cf'; + + // check for legacy sidecar action + const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionSO.hits.hits.length).to.eql(1); + + // check for legacy notification SO + const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); + expect(legacyNotificationSO.hits.hits.length).to.eql(1); + + // patch enable the rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: ruleId, enabled: false }) + .expect(200); + + const { + hits: { + hits: [{ _source: ruleSO }], + }, + } = await getRuleSOById(es, soId); + + // Sidecar should be removed + const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); + + // Legacy notification should be removed + const legacyNotificationSOAfterMigration = await getLegacyActionNotificationSOById(es, soId); + expect(legacyNotificationSOAfterMigration.hits.hits.length).to.eql(0); + + expect(ruleSO?.alert.actions).to.eql([ + { + actionRef: 'action_0', + actionTypeId: '.email', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + subject: 'Test Actions', + to: ['test@test.com'], + }, + }, + ]); + expect(ruleSO?.alert.throttle).to.eql('1d'); + expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.references).to.eql([ + { + id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', + name: 'action_0', + type: 'action', + }, + ]); + }); + + it('migrates legacy actions for rule with action run weekly', async () => { + const soId = '61ec7a40-b076-11ec-bb3f-1f063f8e06cf'; + const ruleId = '05fbdd2a-e802-420b-bdc3-95ae0acca454'; + const legacySidecarId = '63aa2fd0-b076-11ec-bb3f-1f063f8e06cf'; + + // check for legacy sidecar action + const sidecarActionSO = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionSO.hits.hits.length).to.eql(1); + + // check for legacy notification SO + const legacyNotificationSO = await getLegacyActionNotificationSOById(es, soId); + expect(legacyNotificationSO.hits.hits.length).to.eql(1); + + // patch enable the rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: ruleId, enabled: false }) + .expect(200); + + const { + hits: { + hits: [{ _source: ruleSO }], + }, + } = await getRuleSOById(es, soId); + + // Sidecar should be removed + const sidecarActionsSOAfterMigration = await getLegacyActionSOById(es, legacySidecarId); + expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); + + // Legacy notification should be removed + const legacyNotificationSOAfterMigration = await getLegacyActionNotificationSOById(es, soId); + expect(legacyNotificationSOAfterMigration.hits.hits.length).to.eql(0); + + expect(ruleSO?.alert.actions).to.eql([ + { + actionRef: 'action_0', + actionTypeId: '.email', + group: 'default', + params: { + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', + subject: 'Test Actions', + to: ['test@test.com'], + }, + }, + ]); + expect(ruleSO?.alert.throttle).to.eql('7d'); + expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.references).to.eql([ + { + id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', + name: 'action_0', + type: 'action', + }, + ]); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/migrations.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/migrations.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/open_close_signals.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/open_close_signals.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/patch_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/patch_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/patch_rules_bulk.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/patch_rules_bulk.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/patch_rules_bulk.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/perform_bulk_action.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/preview_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/preview_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/preview_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/preview_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_privileges.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/read_privileges.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_privileges.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/read_privileges.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/read_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/read_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/read_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/resolve_read_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/resolve_read_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/runtime.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/runtime.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/runtime.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/runtime.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/template_data/execution_events.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/template_data/execution_events.ts similarity index 83% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/template_data/execution_events.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/template_data/execution_events.ts index c4767bbcc5632..4a674c52fa1ed 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/template_data/execution_events.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/template_data/execution_events.ts @@ -5,6 +5,20 @@ * 2.0. */ +/** + * When using these execution events as templates be sure to replace all the following fields with their updated values + * + * E.g. + * set(e, '@timestamp', dateTimes[i]); + * set(e, 'event.start', dateTimes[i]); + * set(e, 'event.end', dateTimes[i]); + * set(e, 'rule.id', id); + * set(e, 'kibana.saved_objects[0].id', id); + */ + +/** + * Rule executed without issue + */ export const successfulExecution = [ { '@timestamp': '2022-03-17T22:59:31.360Z', @@ -226,30 +240,24 @@ export const successfulExecution = [ }, ]; +/** + * Rule execution identified gap since last execution + */ export const failedGapExecution = [ { - '@timestamp': '2022-03-17T12:36:16.413Z', + '@timestamp': '2022-03-17T12:36:14.868Z', event: { provider: 'alerting', - action: 'execute', + action: 'execute-start', kind: 'alert', category: ['siem'], start: '2022-03-17T12:36:14.868Z', - outcome: 'success', - end: '2022-03-17T12:36:16.413Z', - duration: 1545000000, }, kibana: { alert: { rule: { execution: { uuid: '38fa2d4a-94d3-4ea3-80d6-d1284eb98357', - metrics: { - number_of_triggered_actions: 0, - number_of_searches: 6, - es_search_duration_ms: 2, - total_search_duration_ms: 15, - }, }, }, }, @@ -265,9 +273,6 @@ export const failedGapExecution = [ scheduled: '2022-03-17T12:27:10.060Z', schedule_delay: 544808000000, }, - alerting: { - status: 'ok', - }, server_uuid: '5b2de169-2785-441b-ae8c-186a1936b17d', version: '8.2.0', }, @@ -276,22 +281,21 @@ export const failedGapExecution = [ license: 'basic', category: 'siem.queryRule', ruleset: 'siem', - name: 'Lots of Execution Events', }, - message: - "rule executed: siem.queryRule:fb1fc150-a292-11ec-a2cf-c1b28b0392b0: 'Lots of Execution Events'", + message: 'rule execution start: "fb1fc150-a292-11ec-a2cf-c1b28b0392b0"', ecs: { version: '1.8.0', }, }, { - '@timestamp': '2022-03-17T12:36:15.382Z', + '@timestamp': '2022-03-17T12:36:14.888Z', event: { provider: 'securitySolution.ruleExecution', - kind: 'metric', - action: 'execution-metrics', - sequence: 1, + kind: 'event', + action: 'status-change', + sequence: 0, }, + message: '', rule: { id: 'fb1fc150-a292-11ec-a2cf-c1b28b0392b0', name: 'Lots of Execution Events', @@ -301,9 +305,8 @@ export const failedGapExecution = [ alert: { rule: { execution: { - metrics: { - execution_gap_duration_s: 245, - }, + status: 'running', + status_order: 15, uuid: '38fa2d4a-94d3-4ea3-80d6-d1284eb98357', }, }, @@ -364,14 +367,13 @@ export const failedGapExecution = [ }, }, { - '@timestamp': '2022-03-17T12:36:14.888Z', + '@timestamp': '2022-03-17T12:36:15.382Z', event: { provider: 'securitySolution.ruleExecution', - kind: 'event', - action: 'status-change', - sequence: 0, + kind: 'metric', + action: 'execution-metrics', + sequence: 1, }, - message: '', rule: { id: 'fb1fc150-a292-11ec-a2cf-c1b28b0392b0', name: 'Lots of Execution Events', @@ -381,8 +383,9 @@ export const failedGapExecution = [ alert: { rule: { execution: { - status: 'running', - status_order: 15, + metrics: { + execution_gap_duration_s: 245, + }, uuid: '38fa2d4a-94d3-4ea3-80d6-d1284eb98357', }, }, @@ -403,19 +406,28 @@ export const failedGapExecution = [ }, }, { - '@timestamp': '2022-03-17T12:36:14.868Z', + '@timestamp': '2022-03-17T12:36:16.413Z', event: { provider: 'alerting', - action: 'execute-start', + action: 'execute', kind: 'alert', category: ['siem'], start: '2022-03-17T12:36:14.868Z', + outcome: 'success', + end: '2022-03-17T12:36:16.413Z', + duration: 1545000000, }, kibana: { alert: { rule: { execution: { uuid: '38fa2d4a-94d3-4ea3-80d6-d1284eb98357', + metrics: { + number_of_triggered_actions: 0, + number_of_searches: 6, + es_search_duration_ms: 2, + total_search_duration_ms: 15, + }, }, }, }, @@ -431,6 +443,9 @@ export const failedGapExecution = [ scheduled: '2022-03-17T12:27:10.060Z', schedule_delay: 544808000000, }, + alerting: { + status: 'ok', + }, server_uuid: '5b2de169-2785-441b-ae8c-186a1936b17d', version: '8.2.0', }, @@ -439,14 +454,19 @@ export const failedGapExecution = [ license: 'basic', category: 'siem.queryRule', ruleset: 'siem', + name: 'Lots of Execution Events', }, - message: 'rule execution start: "fb1fc150-a292-11ec-a2cf-c1b28b0392b0"', + message: + "rule executed: siem.queryRule:fb1fc150-a292-11ec-a2cf-c1b28b0392b0: 'Lots of Execution Events'", ecs: { version: '1.8.0', }, }, ]; +/** + * Rule execution resulted in partial warning, e.g. missing index pattern + */ export const partialWarningExecution = [ { '@timestamp': '2022-03-16T23:28:36.012Z', @@ -628,3 +648,112 @@ export const partialWarningExecution = [ }, }, ]; + +/** + * Rule execution failed because rule is disabled (configure 1s interval/lookback then rule + * is disabled while running) + */ +export const failedRanAfterDisabled = [ + { + '@timestamp': '2022-04-21T02:00:55.400Z', + event: { + provider: 'alerting', + action: 'execute', + kind: 'alert', + category: ['siem'], + start: '2022-04-21T02:00:55.397Z', + end: '2022-04-21T02:00:55.400Z', + duration: 3000000, + reason: 'disabled', + outcome: 'failure', + }, + kibana: { + alert: { + rule: { + rule_type_id: 'siem.queryRule', + consumer: 'siem', + execution: { + uuid: '50eb8b2e-8334-4387-b77f-d47fdb7fbe2d', + }, + }, + }, + saved_objects: [ + { + rel: 'primary', + type: 'alert', + id: 'a890e240-b9fb-11ec-8598-338317271cf4', + type_id: 'siem.queryRule', + }, + ], + space_ids: ['default'], + task: { + scheduled: '2022-04-21T02:00:53.325Z', + schedule_delay: 2072000000, + }, + alerting: { + status: 'error', + }, + server_uuid: '5b2de169-2785-441b-ae8c-186a1936b17d', + version: '8.3.0', + }, + rule: { + id: 'a890e240-b9fb-11ec-8598-338317271cf4', + license: 'basic', + category: 'siem.queryRule', + ruleset: 'siem', + }, + error: { + message: 'Rule failed to execute because rule ran after it was disabled.', + }, + message: 'siem.queryRule:a890e240-b9fb-11ec-8598-338317271cf4: execution failed', + ecs: { + version: '1.8.0', + }, + }, + { + '@timestamp': '2022-04-21T02:00:55.397Z', + event: { + provider: 'alerting', + action: 'execute-start', + kind: 'alert', + category: ['siem'], + start: '2022-04-21T02:00:55.397Z', + }, + kibana: { + alert: { + rule: { + rule_type_id: 'siem.queryRule', + consumer: 'siem', + execution: { + uuid: '50eb8b2e-8334-4387-b77f-d47fdb7fbe2d', + }, + }, + }, + saved_objects: [ + { + rel: 'primary', + type: 'alert', + id: 'a890e240-b9fb-11ec-8598-338317271cf4', + type_id: 'siem.queryRule', + }, + ], + space_ids: ['default'], + task: { + scheduled: '2022-04-21T02:00:53.325Z', + schedule_delay: 2072000000, + }, + server_uuid: '5b2de169-2785-441b-ae8c-186a1936b17d', + version: '8.3.0', + }, + rule: { + id: 'a890e240-b9fb-11ec-8598-338317271cf4', + license: 'basic', + category: 'siem.queryRule', + ruleset: 'siem', + }, + message: 'rule execution start: "a890e240-b9fb-11ec-8598-338317271cf4"', + ecs: { + version: '1.8.0', + }, + }, +]; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/throttle.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/throttle.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/throttle.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/throttle.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/timestamps.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/timestamps.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/timestamps.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/timestamps.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_actions.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_actions.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_rules_bulk.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_rules_bulk.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group1/update_rules_bulk.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group2/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group2/config.ts new file mode 100644 index 0000000000000..2430b8f2148d9 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group2/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_endpoint_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group2/create_endpoint_exceptions.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_endpoint_exceptions.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group2/create_endpoint_exceptions.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group2/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group2/index.ts new file mode 100644 index 0000000000000..f477b23e801f3 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group2/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 2', function () { + loadTestFile(require.resolve('./create_endpoint_exceptions')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/config.ts new file mode 100644 index 0000000000000..2430b8f2148d9 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group3/create_exceptions.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/index.ts new file mode 100644 index 0000000000000..10d4e9c2d6635 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group3/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 3', function () { + loadTestFile(require.resolve('./create_exceptions')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/config.ts new file mode 100644 index 0000000000000..2430b8f2148d9 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/index.ts new file mode 100644 index 0000000000000..9394e81017aba --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 4', function () { + loadTestFile(require.resolve('./telemetry')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/README.md similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/README.md diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/index.ts similarity index 51% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/index.ts index d2050179abd0e..4e180f73dc742 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/index.ts @@ -10,15 +10,12 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('Detection rule type telemetry', function () { - describe('', function () { - this.tags('ciGroup28'); - loadTestFile(require.resolve('./usage_collector/all_types')); - loadTestFile(require.resolve('./usage_collector/detection_rules')); - loadTestFile(require.resolve('./usage_collector/detection_rule_status')); + loadTestFile(require.resolve('./usage_collector/all_types')); + loadTestFile(require.resolve('./usage_collector/detection_rules')); + loadTestFile(require.resolve('./usage_collector/detection_rule_status')); - loadTestFile(require.resolve('./task_based/all_types')); - loadTestFile(require.resolve('./task_based/detection_rules')); - loadTestFile(require.resolve('./task_based/security_lists')); - }); + loadTestFile(require.resolve('./task_based/all_types')); + loadTestFile(require.resolve('./task_based/detection_rules')); + loadTestFile(require.resolve('./task_based/security_lists')); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/all_types.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/all_types.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/all_types.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/all_types.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/detection_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/detection_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/detection_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/detection_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/security_lists.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/security_lists.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/task_based/security_lists.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/task_based/security_lists.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/all_types.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/all_types.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/all_types.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/all_types.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rule_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rule_status.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rule_status.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rule_status.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rules.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/usage_collector/detection_rules.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group4/telemetry/usage_collector/detection_rules.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/config.ts new file mode 100644 index 0000000000000..2430b8f2148d9 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/index.ts new file mode 100644 index 0000000000000..ac107392d4b5c --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 5', function () { + loadTestFile(require.resolve('./keyword_family')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/README.md b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/README.md similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/README.md rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/README.md diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/const_keyword.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/const_keyword.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/const_keyword.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/const_keyword.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/index.ts similarity index 68% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/index.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/index.ts index 4855524d650ef..1ecb06fbed4e5 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/index.ts @@ -10,12 +10,8 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('Detection keyword family data types', function () { - describe('', function () { - this.tags('ciGroup11'); - - loadTestFile(require.resolve('./keyword')); - loadTestFile(require.resolve('./const_keyword')); - loadTestFile(require.resolve('./keyword_mixed_with_const')); - }); + loadTestFile(require.resolve('./keyword')); + loadTestFile(require.resolve('./const_keyword')); + loadTestFile(require.resolve('./keyword_mixed_with_const')); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/keyword.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/keyword.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword_mixed_with_const.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/keyword_mixed_with_const.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/keyword_family/keyword_mixed_with_const.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group5/keyword_family/keyword_mixed_with_const.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/alerts/alerts_compatibility.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/alerts/alerts_compatibility.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/alerts/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/index.ts similarity index 79% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/alerts/index.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/index.ts index b06d6cb26e33b..320650a4c79e3 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/alerts/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/index.ts @@ -10,10 +10,6 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('Detection engine signals/alerts compatibility', function () { - describe('', function () { - this.tags('ciGroup11'); - - loadTestFile(require.resolve('./alerts_compatibility')); - }); + loadTestFile(require.resolve('./alerts_compatibility')); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/config.ts new file mode 100644 index 0000000000000..2430b8f2148d9 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/index.ts new file mode 100644 index 0000000000000..92c235e95c0e4 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 6', function () { + loadTestFile(require.resolve('./alerts')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/config.ts new file mode 100644 index 0000000000000..2430b8f2148d9 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/date.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/date.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/date.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/date.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/double.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/double.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/float.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/float.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/index.ts new file mode 100644 index 0000000000000..6b2cf915cc51b --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Detection exceptions data types and operators', function () { + loadTestFile(require.resolve('./date')); + loadTestFile(require.resolve('./double')); + loadTestFile(require.resolve('./float')); + loadTestFile(require.resolve('./integer')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/integer.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group7/exception_operators_data_types/integer.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/index.ts new file mode 100644 index 0000000000000..96f53c47441ed --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group7/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 7', function () { + loadTestFile(require.resolve('./exception_operators_data_types')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/config.ts new file mode 100644 index 0000000000000..2430b8f2148d9 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/index.ts new file mode 100644 index 0000000000000..cf87276aac3ae --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Detection exceptions data types and operators', function () { + loadTestFile(require.resolve('./keyword')); + loadTestFile(require.resolve('./keyword_array')); + loadTestFile(require.resolve('./long')); + loadTestFile(require.resolve('./text')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/keyword.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/keyword.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/keyword_array.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/keyword_array.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/long.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/long.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/text.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group8/exception_operators_data_types/text.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/index.ts new file mode 100644 index 0000000000000..7182e411a1332 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group8/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 8', function () { + loadTestFile(require.resolve('./exception_operators_data_types')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/config.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/config.ts new file mode 100644 index 0000000000000..2430b8f2148d9 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../config.base.ts')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/index.ts new file mode 100644 index 0000000000000..22864980e2653 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Detection exceptions data types and operators', function () { + loadTestFile(require.resolve('./text_array')); + loadTestFile(require.resolve('./ip')); + loadTestFile(require.resolve('./ip_array')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/ip.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/ip.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/ip_array.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/ip_array.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/text_array.ts similarity index 100% rename from x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts rename to x-pack/test/detection_engine_api_integration/security_and_spaces/group9/exception_operators_data_types/text_array.ts diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/index.ts new file mode 100644 index 0000000000000..ad5e427c69df8 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group9/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('detection engine api security and spaces enabled - Group 9', function () { + loadTestFile(require.resolve('./exception_operators_data_types')); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/README.md b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/README.md deleted file mode 100644 index d6beb912d7007..0000000000000 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/README.md +++ /dev/null @@ -1,21 +0,0 @@ -These are tests for rule exception lists where we test each data type -* date -* double -* float -* integer -* ip -* keyword -* long -* text - -Against the operator types of: -* "is" -* "is not" -* "is one of" -* "is not one of" -* "exists" -* "does not exist" -* "is in list" -* "is not in list" - -If you add a test here, ensure you add it to the ./index.ts" file as well \ No newline at end of file diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts deleted file mode 100644 index ded3df8b6716c..0000000000000 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts +++ /dev/null @@ -1,44 +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 { FtrProviderContext } from '../../../common/ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default ({ loadTestFile }: FtrProviderContext): void => { - describe('Detection exceptions data types and operators', function () { - describe('', function () { - this.tags('ciGroup23'); - - loadTestFile(require.resolve('./date')); - loadTestFile(require.resolve('./double')); - loadTestFile(require.resolve('./float')); - loadTestFile(require.resolve('./integer')); - }); - - describe('', function () { - this.tags('ciGroup24'); - - loadTestFile(require.resolve('./keyword')); - loadTestFile(require.resolve('./keyword_array')); - loadTestFile(require.resolve('./long')); - loadTestFile(require.resolve('./text')); - loadTestFile(require.resolve('./text_array')); - }); - - describe('', function () { - this.tags('ciGroup16'); - - loadTestFile(require.resolve('./ip')); - }); - - describe('', function () { - this.tags('ciGroup21'); - - loadTestFile(require.resolve('./ip_array')); - }); - }); -}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts deleted file mode 100644 index a3c4dd8ed3be1..0000000000000 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ /dev/null @@ -1,88 +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 { FtrProviderContext } from '../../common/ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default ({ loadTestFile }: FtrProviderContext): void => { - describe('detection engine api security and spaces enabled', function () { - describe('', function () { - this.tags('ciGroup11'); - - loadTestFile(require.resolve('./aliases')); - loadTestFile(require.resolve('./add_actions')); - loadTestFile(require.resolve('./update_actions')); - loadTestFile(require.resolve('./add_prepackaged_rules')); - loadTestFile(require.resolve('./check_privileges')); - loadTestFile(require.resolve('./create_index')); - loadTestFile(require.resolve('./create_rules')); - loadTestFile(require.resolve('./preview_rules')); - loadTestFile(require.resolve('./create_rules_bulk')); - loadTestFile(require.resolve('./create_ml')); - loadTestFile(require.resolve('./create_threat_matching')); - loadTestFile(require.resolve('./delete_rules')); - loadTestFile(require.resolve('./delete_rules_bulk')); - loadTestFile(require.resolve('./export_rules')); - loadTestFile(require.resolve('./find_rules')); - loadTestFile(require.resolve('./generating_signals')); - loadTestFile(require.resolve('./get_prepackaged_rules_status')); - loadTestFile(require.resolve('./get_rule_execution_events')); - loadTestFile(require.resolve('./import_rules')); - loadTestFile(require.resolve('./import_export_rules')); - loadTestFile(require.resolve('./read_rules')); - loadTestFile(require.resolve('./resolve_read_rules')); - loadTestFile(require.resolve('./update_rules')); - loadTestFile(require.resolve('./update_rules_bulk')); - loadTestFile(require.resolve('./patch_rules_bulk')); - loadTestFile(require.resolve('./perform_bulk_action')); - loadTestFile(require.resolve('./patch_rules')); - loadTestFile(require.resolve('./read_privileges')); - loadTestFile(require.resolve('./open_close_signals')); - loadTestFile(require.resolve('./get_signals_migration_status')); - loadTestFile(require.resolve('./create_signals_migrations')); - loadTestFile(require.resolve('./finalize_signals_migrations')); - loadTestFile(require.resolve('./delete_signals_migrations')); - loadTestFile(require.resolve('./timestamps')); - loadTestFile(require.resolve('./runtime')); - loadTestFile(require.resolve('./throttle')); - loadTestFile(require.resolve('./ignore_fields')); - loadTestFile(require.resolve('./migrations')); - }); - - describe('', function () { - this.tags('ciGroup26'); - - loadTestFile(require.resolve('./create_endpoint_exceptions')); - }); - - describe('', function () { - this.tags('ciGroup14'); - - loadTestFile(require.resolve('./create_exceptions')); - }); - - // That split here enable us on using a different ciGroup to run the tests - // listed on ./exception_operators_data_types/index - describe('', function () { - loadTestFile(require.resolve('./exception_operators_data_types')); - }); - - // That split here enable us on using a different ciGroup to run the tests - // listed on ./keyword_family/index - describe('', function () { - loadTestFile(require.resolve('./keyword_family')); - }); - - describe('', function () { - loadTestFile(require.resolve('./alerts')); - }); - - describe('', function () { - loadTestFile(require.resolve('./telemetry')); - }); - }); -}; diff --git a/x-pack/test/detection_engine_api_integration/utils/get_legacy_action_notification_so.ts b/x-pack/test/detection_engine_api_integration/utils/get_legacy_action_notification_so.ts new file mode 100644 index 0000000000000..9a084d800a2d8 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_legacy_action_notification_so.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Client } from '@elastic/elasticsearch'; +import { SavedObjectReference } from '@kbn/core/server'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { LegacyRuleNotificationAlertTypeParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/notifications/legacy_types'; + +interface LegacyActionNotificationSO extends LegacyRuleNotificationAlertTypeParams { + references: SavedObjectReference[]; +} + +/** + * Fetch all legacy action sidecar notification SOs from the .kibana index + * @param es The ElasticSearch service + */ +export const getLegacyActionNotificationSO = async ( + es: Client +): Promise> => + es.search({ + index: '.kibana', + q: 'alert.alertTypeId:siem.notifications', + }); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_legacy_action_notifications_so_by_id.ts b/x-pack/test/detection_engine_api_integration/utils/get_legacy_action_notifications_so_by_id.ts new file mode 100644 index 0000000000000..3120e85e899bf --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_legacy_action_notifications_so_by_id.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Client } from '@elastic/elasticsearch'; +import { SavedObjectReference } from '@kbn/core/server'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { LegacyRuleNotificationAlertTypeParams } from '@kbn/security-solution-plugin/server/lib/detection_engine/notifications/legacy_types'; + +interface LegacyActionNotificationSO extends LegacyRuleNotificationAlertTypeParams { + references: SavedObjectReference[]; +} + +/** + * Fetch legacy action sidecar notification SOs from the .kibana index + * @param es The ElasticSearch service + * @param id SO id + */ +export const getLegacyActionNotificationSOById = async ( + es: Client, + id: string +): Promise> => + es.search({ + index: '.kibana', + q: `alert.alertTypeId:siem.notifications AND alert.params.ruleAlertId:"${id}"`, + }); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_legacy_actions_so_by_id.ts b/x-pack/test/detection_engine_api_integration/utils/get_legacy_actions_so_by_id.ts new file mode 100644 index 0000000000000..48ea142aa28ca --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_legacy_actions_so_by_id.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Client } from '@elastic/elasticsearch'; +import { SavedObjectReference } from '@kbn/core/server'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { LegacyRuleActions } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_actions/legacy_types'; + +interface LegacyActionSO extends LegacyRuleActions { + references: SavedObjectReference[]; +} + +/** + * Fetch legacy action sidecar SOs from the .kibana index + * @param es The ElasticSearch service + * @param id SO id + */ +export const getLegacyActionSOById = async ( + es: Client, + id: string +): Promise> => + es.search({ + index: '.kibana', + q: `type:siem-detection-engine-rule-actions AND _id:"siem-detection-engine-rule-actions:${id}"`, + }); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_rule_so_by_id.ts b/x-pack/test/detection_engine_api_integration/utils/get_rule_so_by_id.ts new file mode 100644 index 0000000000000..5a46f96cc400a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_rule_so_by_id.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Client } from '@elastic/elasticsearch'; +import { SavedObjectReference } from '@kbn/core/server'; +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Rule } from '@kbn/alerting-plugin/common'; + +interface RuleSO { + alert: Rule; + references: SavedObjectReference[]; +} + +/** + * Fetch legacy action sidecar SOs from the .kibana index + * @param es The ElasticSearch service + * @param id SO id + */ +export const getRuleSOById = async (es: Client, id: string): Promise> => + es.search({ + index: '.kibana', + q: `type:alert AND _id:"alert:${id}"`, + }); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_slack_action.ts b/x-pack/test/detection_engine_api_integration/utils/get_slack_action.ts new file mode 100644 index 0000000000000..1d88f2cdbce73 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_slack_action.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const getSlackAction = () => ({ + actionTypeId: '.slack', + secrets: { + webhookUrl: 'http://localhost:123', + }, + name: 'Slack connector', +}); diff --git a/x-pack/test/detection_engine_api_integration/utils/index.ts b/x-pack/test/detection_engine_api_integration/utils/index.ts index 3fb9c8ebafc02..81bad1b1b583b 100644 --- a/x-pack/test/detection_engine_api_integration/utils/index.ts +++ b/x-pack/test/detection_engine_api_integration/utils/index.ts @@ -35,7 +35,10 @@ export * from './get_detection_metrics_from_body'; export * from './get_eql_rule_for_signal_testing'; export * from './get_event_log_execute_complete_by_id'; export * from './get_index_name_from_load'; +export * from './get_legacy_action_notification_so'; +export * from './get_legacy_action_notifications_so_by_id'; export * from './get_legacy_action_so'; +export * from './get_legacy_actions_so_by_id'; export * from './get_open_signals'; export * from './get_prepackaged_rule_status'; export * from './get_query_all_signals'; @@ -44,6 +47,7 @@ export * from './get_query_signals_ids'; export * from './get_query_signals_rule_id'; export * from './get_rule'; export * from './get_rule_for_signal_testing'; +export * from './get_rule_so_by_id'; export * from './get_rule_for_signal_testing_with_timestamp_override'; export * from './get_rule_with_web_hook_action'; export * from './get_saved_query_rule_for_signal_testing'; @@ -70,6 +74,7 @@ export * from './get_stats'; export * from './get_stats_url'; export * from './get_threat_match_rule_for_signal_testing'; export * from './get_threshold_rule_for_signal_testing'; +export * from './get_slack_action'; export * from './get_web_hook_action'; export * from './index_event_log_execution_events'; export * from './install_prepackaged_rules'; diff --git a/x-pack/test/detection_engine_api_integration/utils/index_event_log_execution_events.ts b/x-pack/test/detection_engine_api_integration/utils/index_event_log_execution_events.ts index 63f8c9f2c8e1b..8e354607d4002 100644 --- a/x-pack/test/detection_engine_api_integration/utils/index_event_log_execution_events.ts +++ b/x-pack/test/detection_engine_api_integration/utils/index_event_log_execution_events.ts @@ -19,8 +19,9 @@ export const indexEventLogExecutionEvents = async ( log: ToolingLog, events: object[] ): Promise => { + const aliases = await es.cat.aliases({ format: 'json', name: '.kibana-event-log-*' }); const operations = events.flatMap((doc: object) => [ - { index: { _index: '.kibana-event-log-*' } }, + { index: { _index: aliases[0].index } }, doc, ]); diff --git a/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts index de87d627ac486..0313187ac09a7 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('encryptedSavedObjects', function encryptedSavedObjectsSuite() { - this.tags('ciGroup13'); loadTestFile(require.resolve('./encrypted_saved_objects_api')); loadTestFile(require.resolve('./encrypted_saved_objects_decryption')); }); diff --git a/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts b/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts index 1ca035df18fd5..10a4e18f8c133 100644 --- a/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts +++ b/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function endpointAPIIntegrationTests({ loadTestFile }: FtrProviderContext) { // Failing ES snapshot promotion: https://github.com/elastic/kibana/issues/70535 describe.skip('Endpoint plugin', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./metadata')); }); } diff --git a/x-pack/test/examples/config.ts b/x-pack/test/examples/config.ts index 6eb1e86dd0826..16db620e76598 100644 --- a/x-pack/test/examples/config.ts +++ b/x-pack/test/examples/config.ts @@ -12,7 +12,9 @@ import fs from 'fs'; import { KIBANA_ROOT } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); // Find all folders in /examples and /x-pack/examples since we treat all them as plugin folder const examplesFiles = fs.readdirSync(resolve(KIBANA_ROOT, 'examples')); diff --git a/x-pack/test/examples/embedded_lens/index.ts b/x-pack/test/examples/embedded_lens/index.ts index debab4773f9eb..b418e69584a9a 100644 --- a/x-pack/test/examples/embedded_lens/index.ts +++ b/x-pack/test/examples/embedded_lens/index.ts @@ -13,6 +13,8 @@ export default function ({ getService, loadTestFile }: PluginFunctionalProviderC const kibanaServer = getService('kibanaServer'); describe('embedded Lens examples', function () { + this.tags('skipFirefox'); + before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.importExport.load( @@ -30,10 +32,6 @@ export default function ({ getService, loadTestFile }: PluginFunctionalProviderC ); }); - describe('', function () { - this.tags(['ciGroup4', 'skipFirefox']); - - loadTestFile(require.resolve('./embedded_example')); - }); + loadTestFile(require.resolve('./embedded_example')); }); } diff --git a/x-pack/test/examples/reporting_examples/index.ts b/x-pack/test/examples/reporting_examples/index.ts index e4e5c93e5eee2..2f1b00597a9d8 100644 --- a/x-pack/test/examples/reporting_examples/index.ts +++ b/x-pack/test/examples/reporting_examples/index.ts @@ -10,8 +10,6 @@ import { PluginFunctionalProviderContext } from '../../../../test/plugin_functio // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile }: PluginFunctionalProviderContext) { describe('reporting examples', function () { - this.tags('ciGroup13'); - loadTestFile(require.resolve('./capture_test')); }); } diff --git a/x-pack/test/examples/screenshotting/index.ts b/x-pack/test/examples/screenshotting/index.ts index e9ecd2ecde346..c64d84c7fcf3d 100644 --- a/x-pack/test/examples/screenshotting/index.ts +++ b/x-pack/test/examples/screenshotting/index.ts @@ -22,8 +22,6 @@ export default function ({ // FAILING: https://github.com/elastic/kibana/issues/131190 describe.skip('Screenshotting Example', function () { - this.tags('ciGroup13'); - before(async () => { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json'); diff --git a/x-pack/test/examples/search_examples/index.ts b/x-pack/test/examples/search_examples/index.ts index c0f8ab22c8f8f..cee873dfed53a 100644 --- a/x-pack/test/examples/search_examples/index.ts +++ b/x-pack/test/examples/search_examples/index.ts @@ -13,7 +13,6 @@ export default function ({ getService, loadTestFile }: PluginFunctionalProviderC const kibanaServer = getService('kibanaServer'); describe('search examples', function () { - this.tags('ciGroup13'); before(async () => { await esArchiver.emptyKibanaIndex(); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index 8f2b3effd94ed..16f8fc04aa92f 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -569,6 +569,10 @@ const expectAssetsInstalled = ({ id: 'metrics-all_assets.test_metrics-all_assets', type: 'data_stream_ilm_policy', }, + { + id: 'all_assets', + type: 'ilm_policy', + }, { id: 'logs-all_assets.test_logs', type: 'index_template', diff --git a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts index b73ca9537990c..9758107cee83d 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts @@ -403,13 +403,17 @@ export default function (providerContext: FtrProviderContext) { ], installed_es: [ { - id: 'logs-all_assets.test_logs-all_assets', - type: 'data_stream_ilm_policy', + id: 'all_assets', + type: 'ilm_policy', }, { id: 'default', type: 'ml_model', }, + { + id: 'logs-all_assets.test_logs-all_assets', + type: 'data_stream_ilm_policy', + }, { id: 'logs-all_assets.test_logs-0.2.0', type: 'ingest_pipeline', diff --git a/x-pack/test/fleet_api_integration/apis/index.js b/x-pack/test/fleet_api_integration/apis/index.js index 9f68eb1c7f81f..1c528e719e2e8 100644 --- a/x-pack/test/fleet_api_integration/apis/index.js +++ b/x-pack/test/fleet_api_integration/apis/index.js @@ -9,8 +9,6 @@ import { setupTestUsers } from './test_users'; export default function ({ loadTestFile, getService }) { describe('Fleet Endpoints', function () { - this.tags('ciGroup29'); - before(async () => { await setupTestUsers(getService('security')); }); diff --git a/x-pack/test/fleet_cypress/config.ts b/x-pack/test/fleet_cypress/config.ts index d2076fa940412..52198f4f035e0 100644 --- a/x-pack/test/fleet_cypress/config.ts +++ b/x-pack/test/fleet_cypress/config.ts @@ -14,7 +14,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') + require.resolve('../functional/config.base.js') ); return { diff --git a/x-pack/test/fleet_functional/apps/fleet/agents_page.ts b/x-pack/test/fleet_functional/apps/fleet/agents_page.ts index 515eaa65f5310..cff1273e0b901 100644 --- a/x-pack/test/fleet_functional/apps/fleet/agents_page.ts +++ b/x-pack/test/fleet_functional/apps/fleet/agents_page.ts @@ -11,8 +11,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const { agentsPage } = getPageObjects(['agentsPage']); describe('When in the Fleet application', function () { - this.tags(['ciGroup7']); - describe('and on the agents page', () => { before(async () => { await agentsPage.navigateToAgentsPage(); diff --git a/x-pack/test/fleet_functional/apps/fleet/index.ts b/x-pack/test/fleet_functional/apps/fleet/index.ts index ec16e2d857183..965d4c7776197 100644 --- a/x-pack/test/fleet_functional/apps/fleet/index.ts +++ b/x-pack/test/fleet_functional/apps/fleet/index.ts @@ -11,7 +11,6 @@ export default function (providerContext: FtrProviderContext) { const { loadTestFile } = providerContext; describe('endpoint', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./agents_page')); }); } diff --git a/x-pack/test/fleet_functional/apps/home/index.ts b/x-pack/test/fleet_functional/apps/home/index.ts index cd14bfdff557d..727213b96349e 100644 --- a/x-pack/test/fleet_functional/apps/home/index.ts +++ b/x-pack/test/fleet_functional/apps/home/index.ts @@ -11,7 +11,6 @@ export default function (providerContext: FtrProviderContext) { const { loadTestFile } = providerContext; describe('home onboarding', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./welcome')); }); } diff --git a/x-pack/test/fleet_functional/config.ts b/x-pack/test/fleet_functional/config.ts index 60db783280aec..5efc39b02acd6 100644 --- a/x-pack/test/fleet_functional/config.ts +++ b/x-pack/test/fleet_functional/config.ts @@ -11,7 +11,9 @@ import { pageObjects } from './page_objects'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); return { ...xpackFunctionalConfig.getAll(), diff --git a/x-pack/test/functional/apps/advanced_settings/config.ts b/x-pack/test/functional/apps/advanced_settings/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/advanced_settings/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/advanced_settings/index.ts b/x-pack/test/functional/apps/advanced_settings/index.ts index d962f648db395..f121b031466ea 100644 --- a/x-pack/test/functional/apps/advanced_settings/index.ts +++ b/x-pack/test/functional/apps/advanced_settings/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function advancedSettingsApp({ loadTestFile }: FtrProviderContext) { describe('Advanced Settings', function canvasAppTestSuite() { - this.tags(['ciGroup2', 'skipFirefox']); // CI requires tags ヽ(゜Q。)ノ? + this.tags(['skipFirefox']); // CI requires tags ヽ(゜Q。)ノ? loadTestFile(require.resolve('./feature_controls/advanced_settings_security')); loadTestFile(require.resolve('./feature_controls/advanced_settings_spaces')); }); diff --git a/x-pack/test/functional/apps/api_keys/config.ts b/x-pack/test/functional/apps/api_keys/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/api_keys/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/api_keys/index.ts b/x-pack/test/functional/apps/api_keys/index.ts index 7b9afd201e3d9..2f9d7206d374a 100644 --- a/x-pack/test/functional/apps/api_keys/index.ts +++ b/x-pack/test/functional/apps/api_keys/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('API Keys app', function () { - this.tags(['ciGroup7']); loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./feature_controls')); }); diff --git a/x-pack/test/functional/apps/apm/config.ts b/x-pack/test/functional/apps/apm/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/apm/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/apm/index.ts b/x-pack/test/functional/apps/apm/index.ts index 20c2bc264a74f..61aca7ca3f9de 100644 --- a/x-pack/test/functional/apps/apm/index.ts +++ b/x-pack/test/functional/apps/apm/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('APM specs', function () { - this.tags('ciGroup10'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./correlations')); }); diff --git a/x-pack/test/functional/apps/canvas/config.ts b/x-pack/test/functional/apps/canvas/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/canvas/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/canvas/embeddables/lens.ts b/x-pack/test/functional/apps/canvas/embeddables/lens.ts index 5ecd3a3156909..748f17c720b53 100644 --- a/x-pack/test/functional/apps/canvas/embeddables/lens.ts +++ b/x-pack/test/functional/apps/canvas/embeddables/lens.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function canvasLensTest({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); - const PageObjects = getPageObjects(['canvas', 'common', 'header', 'lens']); + const PageObjects = getPageObjects(['canvas', 'common', 'header', 'lens', 'unifiedSearch']); const esArchiver = getService('esArchiver'); const dashboardAddPanel = getService('dashboardAddPanel'); const dashboardPanelActions = getService('dashboardPanelActions'); @@ -68,6 +68,7 @@ export default function canvasLensTest({ getService, getPageObjects }: FtrProvid await PageObjects.canvas.deleteSelectedElement(); const originalEmbeddableCount = await PageObjects.canvas.getEmbeddableCount(); await PageObjects.canvas.createNewVis('lens'); + await PageObjects.unifiedSearch.closeTourPopoverByLocalStorage(); await PageObjects.lens.goToTimeRange(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', diff --git a/x-pack/test/functional/apps/canvas/index.js b/x-pack/test/functional/apps/canvas/index.js index 784aeb6655768..2c6a46b75e510 100644 --- a/x-pack/test/functional/apps/canvas/index.js +++ b/x-pack/test/functional/apps/canvas/index.js @@ -27,7 +27,6 @@ export default function canvasApp({ loadTestFile, getService }) { await security.testUser.restoreDefaults(); }); - this.tags('ciGroup2'); loadTestFile(require.resolve('./smoke_test')); loadTestFile(require.resolve('./expression')); loadTestFile(require.resolve('./filters')); diff --git a/x-pack/test/functional/apps/cross_cluster_replication/config.ts b/x-pack/test/functional/apps/cross_cluster_replication/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/cross_cluster_replication/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/cross_cluster_replication/index.ts b/x-pack/test/functional/apps/cross_cluster_replication/index.ts index 1ab1ab7183394..5c6539b5e73f7 100644 --- a/x-pack/test/functional/apps/cross_cluster_replication/index.ts +++ b/x-pack/test/functional/apps/cross_cluster_replication/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Cross Cluster Replication app', function () { - this.tags(['ciGroup4', 'skipCloud']); + this.tags('skipCloud'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); diff --git a/x-pack/test/functional/apps/dashboard/README.md b/x-pack/test/functional/apps/dashboard/README.md new file mode 100644 index 0000000000000..5e87a8b210bdd --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/README.md @@ -0,0 +1,7 @@ +# What are all these groups? + +These tests take a while so they have been broken up into groups with their own `config.ts` and `index.ts` file, causing each of these groups to be independent bundles of tests which can be run on some worker in CI without taking an incredible amount of time. + +Want to change the groups to something more logical? Have fun! Just make sure that each group executes on CI in less than 10 minutes or so. We don't currently have any mechanism for validating this right now, you just need to look at the times in the log output on CI, but we'll be working on tooling for making this information more accessible soon. + +- Kibana Operations \ No newline at end of file diff --git a/x-pack/test/functional/apps/dashboard/group1/config.ts b/x-pack/test/functional/apps/dashboard/group1/config.ts new file mode 100644 index 0000000000000..d927f93adeffd --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/group1/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts b/x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_dashboard_drilldown.ts similarity index 99% rename from x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts rename to x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_dashboard_drilldown.ts index 9fd6beeb8ff22..c540d5673d89e 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts +++ b/x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_dashboard_drilldown.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const DRILLDOWN_TO_PIE_CHART_NAME = 'Go to pie chart dashboard'; const DRILLDOWN_TO_AREA_CHART_NAME = 'Go to area chart dashboard'; diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts b/x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_url_drilldown.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts rename to x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_url_drilldown.ts index 5ed118c9b753a..ca057b7421b7c 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts +++ b/x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_url_drilldown.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const DRILLDOWN_TO_DISCOVER_URL = 'Go to discover'; diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts b/x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_chart_action.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts rename to x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_chart_action.ts index b9272a0a6c3bd..2e2c1f7ecca1d 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts +++ b/x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_chart_action.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const ACTION_ID = 'ACTION_EXPLORE_DATA_CHART'; const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts b/x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_panel_action.ts similarity index 96% rename from x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts rename to x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_panel_action.ts index c78c716364c4b..1dafddbb8567b 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts +++ b/x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_panel_action.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const ACTION_ID = 'ACTION_EXPLORE_DATA'; const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; @@ -67,7 +67,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ); await discover.waitForDiscoverAppOnScreen(); - const el = await testSubjects.find('indexPattern-switch-link'); + const el = await testSubjects.find('discover-dataView-switch-link'); const text = await el.getVisibleText(); expect(text).to.be('logstash-*'); diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/group1/drilldowns/index.ts similarity index 95% rename from x-pack/test/functional/apps/dashboard/drilldowns/index.ts rename to x-pack/test/functional/apps/dashboard/group1/drilldowns/index.ts index fac0c355ce4d0..eaa1189ab1007 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts +++ b/x-pack/test/functional/apps/dashboard/group1/drilldowns/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile, getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts rename to x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts index efac6d6739fcb..6b08a9455b644 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_security.ts @@ -10,7 +10,7 @@ import { createDashboardEditUrl, DashboardConstants, } from '@kbn/dashboard-plugin/public/dashboard_constants'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -228,6 +228,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { true, false ); + const contextMenuPanelTitleButton = await testSubjects.exists( + 'contextMenuPanelTitleButton' + ); + if (contextMenuPanelTitleButton) { + await testSubjects.click('contextMenuPanelTitleButton'); + } await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); const queryString = await queryBar.getQueryString(); @@ -244,6 +250,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('allow saving currently loaded query as a copy', async () => { await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); + await queryBar.setQuery('response:404'); await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( 'ok2', 'description', diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_spaces.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts rename to x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_spaces.ts index 0d177a0f3c65b..24a90b883315e 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts +++ b/x-pack/test/functional/apps/dashboard/group1/feature_controls/dashboard_spaces.ts @@ -10,7 +10,7 @@ import { createDashboardEditUrl, DashboardConstants, } from '@kbn/dashboard-plugin/public/dashboard_constants'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/index.ts b/x-pack/test/functional/apps/dashboard/group1/feature_controls/index.ts similarity index 89% rename from x-pack/test/functional/apps/dashboard/feature_controls/index.ts rename to x-pack/test/functional/apps/dashboard/group1/feature_controls/index.ts index 3b32ea031f6e2..2ea15966f3740 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/index.ts +++ b/x-pack/test/functional/apps/dashboard/group1/feature_controls/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('feature controls', function () { diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts b/x-pack/test/functional/apps/dashboard/group1/feature_controls/time_to_visualize_security.ts similarity index 99% rename from x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts rename to x-pack/test/functional/apps/dashboard/group1/feature_controls/time_to_visualize_security.ts index 9eeb49f5eb0d2..57ddc76835213 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts +++ b/x-pack/test/functional/apps/dashboard/group1/feature_controls/time_to_visualize_security.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/x-pack/test/functional/apps/dashboard/group1/index.ts b/x-pack/test/functional/apps/dashboard/group1/index.ts new file mode 100644 index 0000000000000..f829002448f33 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/group1/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('dashboard', function () { + loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./preserve_url')); + loadTestFile(require.resolve('./reporting')); + loadTestFile(require.resolve('./drilldowns')); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/preserve_url.ts b/x-pack/test/functional/apps/dashboard/group1/preserve_url.ts similarity index 97% rename from x-pack/test/functional/apps/dashboard/preserve_url.ts rename to x-pack/test/functional/apps/dashboard/group1/preserve_url.ts index e391a8b346f6f..88ad055d34c20 100644 --- a/x-pack/test/functional/apps/dashboard/preserve_url.ts +++ b/x-pack/test/functional/apps/dashboard/group1/preserve_url.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/reporting/README.md b/x-pack/test/functional/apps/dashboard/group1/reporting/README.md similarity index 92% rename from x-pack/test/functional/apps/dashboard/reporting/README.md rename to x-pack/test/functional/apps/dashboard/group1/reporting/README.md index 3a2b8f5cc783f..149b691dc3115 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/README.md +++ b/x-pack/test/functional/apps/dashboard/group1/reporting/README.md @@ -11,8 +11,8 @@ Every now and then visual changes will be made that will require the snapshots t 1. **Load the ES Archive containing the dashboard and data.** This will load the test data into an Elasticsearch instance running via the functional test server: ``` - node scripts/es_archiver load reporting/ecommerce --config=x-pack/test/functional/config.js - node scripts/es_archiver load reporting/ecommerce_kibana --config=x-pack/test/functional/config.js + node scripts/es_archiver load reporting/ecommerce --config=x-pack/test/functional/apps/dashboard/config.ts + node scripts/es_archiver load reporting/ecommerce_kibana --config=x-pack/test/functional/apps/dashboard/config.ts ``` 2. **Generate the reports of the E-commerce dashboard in the Kibana UI.** Navigate to `http://localhost:5620`, find the archived dashboard, and generate all the types of reports for which there are stored baseline images. diff --git a/x-pack/test/functional/apps/dashboard/reporting/__snapshots__/download_csv.snap b/x-pack/test/functional/apps/dashboard/group1/reporting/__snapshots__/download_csv.snap similarity index 99% rename from x-pack/test/functional/apps/dashboard/reporting/__snapshots__/download_csv.snap rename to x-pack/test/functional/apps/dashboard/group1/reporting/__snapshots__/download_csv.snap index d524543183a3f..e6b31be13861d 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/__snapshots__/download_csv.snap +++ b/x-pack/test/functional/apps/dashboard/group1/reporting/__snapshots__/download_csv.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`dashboard Reporting Download CSV Default Saved Search Data Download CSV export of a saved search panel 1`] = ` +exports[`dashboard Reporting Download CSV Default Saved Search Data Download CSV export of a saved search panel 1`] = ` "\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"products.created_on\\",sku \\"Jun 22, 2019 @ 00:00:00.000\\",\\"Men's Clothing, Men's Shoes\\",EUR,23,564670,6,\\"Dec 11, 2016 @ 00:00:00.000, Dec 11, 2016 @ 00:00:00.000\\",\\"ZO0531205312, ZO0684706847\\" \\"Jun 22, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,44,564710,6,\\"Dec 11, 2016 @ 00:00:00.000, Dec 11, 2016 @ 00:00:00.000\\",\\"ZO0263402634, ZO0499404994\\" @@ -460,7 +460,7 @@ exports[`dashboard Reporting Download CSV Default Saved Search Data Download CS " `; -exports[`dashboard Reporting Download CSV Default Saved Search Data Downloads a filtered CSV export of a saved search panel 1`] = ` +exports[`dashboard Reporting Download CSV Default Saved Search Data Downloads a filtered CSV export of a saved search panel 1`] = ` "\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"products.created_on\\",sku \\"Jun 22, 2019 @ 00:00:00.000\\",\\"Men's Clothing, Men's Shoes\\",EUR,23,564670,6,\\"Dec 11, 2016 @ 00:00:00.000, Dec 11, 2016 @ 00:00:00.000\\",\\"ZO0531205312, ZO0684706847\\" \\"Jun 22, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing\\",EUR,52,564513,6,\\"Dec 11, 2016 @ 00:00:00.000, Dec 11, 2016 @ 00:00:00.000\\",\\"ZO0390003900, ZO0287902879\\" @@ -562,13 +562,13 @@ exports[`dashboard Reporting Download CSV Default Saved Search Data Downloads a " `; -exports[`dashboard Reporting Download CSV Field Formatters and Scripted Fields Download CSV export of a saved search panel 1`] = ` +exports[`dashboard Reporting Download CSV Field Formatters and Scripted Fields Download CSV export of a saved search panel 1`] = ` "date,\\"_id\\",name,gender,value,year,\\"years_ago\\",\\"date_informal\\" \\"Jan 1, 1982 @ 00:00:00.000\\",\\"1982-Fethany-F\\",Fethany,F,780,1982,\\"37.00000000000000000000\\",\\"Jan 1st 82\\" " `; -exports[`dashboard Reporting Download CSV Filtered Saved Search Downloads filtered Discover saved search report 1`] = ` +exports[`dashboard Reporting Download CSV Filtered Saved Search Downloads filtered Discover saved search report 1`] = ` "\\"order_date\\",category,\\"customer_full_name\\",\\"taxful_total_price\\",currency \\"Jun 25, 2019 @ 00:00:00.000\\",\\"Women's Accessories\\",\\"Betty Reese\\",\\"22.984\\",EUR \\"Jun 25, 2019 @ 00:00:00.000\\",\\"Women's Accessories, Women's Clothing\\",\\"Betty Brewer\\",\\"28.984\\",EUR diff --git a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/group1/reporting/download_csv.ts similarity index 99% rename from x-pack/test/functional/apps/dashboard/reporting/download_csv.ts rename to x-pack/test/functional/apps/dashboard/group1/reporting/download_csv.ts index 1e2d465fee66b..bdcf95955b451 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/group1/reporting/download_csv.ts @@ -9,7 +9,7 @@ import { REPO_ROOT } from '@kbn/utils'; import expect from '@kbn/expect'; import fs from 'fs'; import path from 'path'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/reporting/index.ts b/x-pack/test/functional/apps/dashboard/group1/reporting/index.ts similarity index 86% rename from x-pack/test/functional/apps/dashboard/reporting/index.ts rename to x-pack/test/functional/apps/dashboard/group1/reporting/index.ts index 088e54d534b6a..ef9821a544b3a 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/index.ts +++ b/x-pack/test/functional/apps/dashboard/group1/reporting/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Reporting', function () { diff --git a/x-pack/test/functional/apps/dashboard/reporting/reports/baseline/large_dashboard_preserve_layout.png b/x-pack/test/functional/apps/dashboard/group1/reporting/reports/baseline/large_dashboard_preserve_layout.png similarity index 100% rename from x-pack/test/functional/apps/dashboard/reporting/reports/baseline/large_dashboard_preserve_layout.png rename to x-pack/test/functional/apps/dashboard/group1/reporting/reports/baseline/large_dashboard_preserve_layout.png diff --git a/x-pack/test/functional/apps/dashboard/reporting/reports/baseline/small_dashboard_preserve_layout.png b/x-pack/test/functional/apps/dashboard/group1/reporting/reports/baseline/small_dashboard_preserve_layout.png similarity index 100% rename from x-pack/test/functional/apps/dashboard/reporting/reports/baseline/small_dashboard_preserve_layout.png rename to x-pack/test/functional/apps/dashboard/group1/reporting/reports/baseline/small_dashboard_preserve_layout.png diff --git a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts b/x-pack/test/functional/apps/dashboard/group1/reporting/screenshots.ts similarity index 99% rename from x-pack/test/functional/apps/dashboard/reporting/screenshots.ts rename to x-pack/test/functional/apps/dashboard/group1/reporting/screenshots.ts index d42d23b7578a5..5a69262801d2e 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts +++ b/x-pack/test/functional/apps/dashboard/group1/reporting/screenshots.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import path from 'path'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const REPORTS_FOLDER = path.resolve(__dirname, 'reports'); diff --git a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/group2/_async_dashboard.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/_async_dashboard.ts rename to x-pack/test/functional/apps/dashboard/group2/_async_dashboard.ts index 89c58645b64b0..ae79c1893ac8b 100644 --- a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts +++ b/x-pack/test/functional/apps/dashboard/group2/_async_dashboard.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/x-pack/test/functional/apps/dashboard/group2/config.ts b/x-pack/test/functional/apps/dashboard/group2/config.ts new file mode 100644 index 0000000000000..d927f93adeffd --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/group2/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts b/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts rename to x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts index 9e4c2554100b9..2604b942f7313 100644 --- a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'dashboard', 'visualize', 'lens', 'timePicker']); diff --git a/x-pack/test/functional/apps/dashboard/dashboard_maps_by_value.ts b/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/dashboard_maps_by_value.ts rename to x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts index e885d1c0d3f73..c1224b604364d 100644 --- a/x-pack/test/functional/apps/dashboard/dashboard_maps_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/x-pack/test/functional/apps/dashboard/dashboard_tagging.ts b/x-pack/test/functional/apps/dashboard/group2/dashboard_tagging.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/dashboard_tagging.ts rename to x-pack/test/functional/apps/dashboard/group2/dashboard_tagging.ts index 784d1f3678415..ca0093135b7da 100644 --- a/x-pack/test/functional/apps/dashboard/dashboard_tagging.ts +++ b/x-pack/test/functional/apps/dashboard/group2/dashboard_tagging.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); diff --git a/x-pack/test/functional/apps/dashboard/group2/index.ts b/x-pack/test/functional/apps/dashboard/group2/index.ts new file mode 100644 index 0000000000000..666756735e80f --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/group2/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('dashboard', function () { + loadTestFile(require.resolve('./sync_colors')); + loadTestFile(require.resolve('./_async_dashboard')); + loadTestFile(require.resolve('./dashboard_tagging')); + loadTestFile(require.resolve('./dashboard_lens_by_value')); + loadTestFile(require.resolve('./dashboard_maps_by_value')); + loadTestFile(require.resolve('./panel_titles')); + + loadTestFile(require.resolve('./migration_smoke_tests/lens_migration_smoke_test')); + loadTestFile(require.resolve('./migration_smoke_tests/controls_migration_smoke_test')); + loadTestFile(require.resolve('./migration_smoke_tests/visualize_migration_smoke_test')); + loadTestFile(require.resolve('./migration_smoke_tests/tsvb_migration_smoke_test')); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/controls_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/controls_migration_smoke_test.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/controls_migration_smoke_test.ts rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/controls_migration_smoke_test.ts index b87ee15910d23..20fe8a9413723 100644 --- a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/controls_migration_smoke_test.ts +++ b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/controls_migration_smoke_test.ts @@ -12,7 +12,7 @@ import expect from '@kbn/expect'; import path from 'path'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/controls_dashboard_migration_test_8_0_0.ndjson b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/controls_dashboard_migration_test_8_0_0.ndjson similarity index 100% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/controls_dashboard_migration_test_8_0_0.ndjson rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/controls_dashboard_migration_test_8_0_0.ndjson diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson similarity index 100% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_12_1.ndjson b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_12_1.ndjson similarity index 100% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_12_1.ndjson rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_12_1.ndjson diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_13_3.ndjson b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_13_3.ndjson similarity index 100% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_13_3.ndjson rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/tsvb_dashboard_migration_test_7_13_3.ndjson diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson similarity index 100% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/exports/visualize_dashboard_migration_test_7_12_1.ndjson diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/lens_migration_smoke_test.ts similarity index 97% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/lens_migration_smoke_test.ts index 78b7ccfe7df08..32c6449ad97b5 100644 --- a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts +++ b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/lens_migration_smoke_test.ts @@ -11,7 +11,7 @@ import expect from '@kbn/expect'; import path from 'path'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/tsvb_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/tsvb_migration_smoke_test.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/tsvb_migration_smoke_test.ts rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/tsvb_migration_smoke_test.ts index 22606cf6d2862..0c3f9c652a6ec 100644 --- a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/tsvb_migration_smoke_test.ts +++ b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/tsvb_migration_smoke_test.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import path from 'path'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/visualize_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/visualize_migration_smoke_test.ts similarity index 97% rename from x-pack/test/functional/apps/dashboard/migration_smoke_tests/visualize_migration_smoke_test.ts rename to x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/visualize_migration_smoke_test.ts index d3d6ca46cd227..f4e86217a4d75 100644 --- a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/visualize_migration_smoke_test.ts +++ b/x-pack/test/functional/apps/dashboard/group2/migration_smoke_tests/visualize_migration_smoke_test.ts @@ -11,7 +11,7 @@ import expect from '@kbn/expect'; import path from 'path'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/panel_titles.ts b/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts similarity index 99% rename from x-pack/test/functional/apps/dashboard/panel_titles.ts rename to x-pack/test/functional/apps/dashboard/group2/panel_titles.ts index 3db72ba8d0a90..34b7e14d26a1a 100644 --- a/x-pack/test/functional/apps/dashboard/panel_titles.ts +++ b/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/sync_colors.ts b/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts similarity index 98% rename from x-pack/test/functional/apps/dashboard/sync_colors.ts rename to x-pack/test/functional/apps/dashboard/group2/sync_colors.ts index a3628635dfa2f..093318bb8b5cd 100644 --- a/x-pack/test/functional/apps/dashboard/sync_colors.ts +++ b/x-pack/test/functional/apps/dashboard/group2/sync_colors.ts @@ -7,7 +7,7 @@ import { DebugState } from '@elastic/charts'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts deleted file mode 100644 index baf37ff56bd80..0000000000000 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ /dev/null @@ -1,35 +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 { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('dashboard', function () { - describe('', function () { - this.tags('ciGroup19'); - loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./preserve_url')); - loadTestFile(require.resolve('./reporting')); - loadTestFile(require.resolve('./drilldowns')); - }); - - describe('', function () { - this.tags('ciGroup31'); - loadTestFile(require.resolve('./sync_colors')); - loadTestFile(require.resolve('./_async_dashboard')); - loadTestFile(require.resolve('./dashboard_tagging')); - loadTestFile(require.resolve('./dashboard_lens_by_value')); - loadTestFile(require.resolve('./dashboard_maps_by_value')); - loadTestFile(require.resolve('./panel_titles')); - - loadTestFile(require.resolve('./migration_smoke_tests/lens_migration_smoke_test')); - loadTestFile(require.resolve('./migration_smoke_tests/controls_migration_smoke_test')); - loadTestFile(require.resolve('./migration_smoke_tests/visualize_migration_smoke_test')); - loadTestFile(require.resolve('./migration_smoke_tests/tsvb_migration_smoke_test')); - }); - }); -} diff --git a/x-pack/test/functional/apps/data_views/config.ts b/x-pack/test/functional/apps/data_views/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/data_views/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/data_views/index.ts b/x-pack/test/functional/apps/data_views/index.ts index 3b3f7b3608113..3284dc901c25a 100644 --- a/x-pack/test/functional/apps/data_views/index.ts +++ b/x-pack/test/functional/apps/data_views/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function advancedSettingsApp({ loadTestFile }: FtrProviderContext) { describe('Data Views', function indexPatternsTestSuite() { - this.tags('ciGroup2'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./spaces')); }); diff --git a/x-pack/test/functional/apps/dev_tools/config.ts b/x-pack/test/functional/apps/dev_tools/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/dev_tools/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/dev_tools/index.ts b/x-pack/test/functional/apps/dev_tools/index.ts index 4f0e9290cc25e..6ef1688bb4c4e 100644 --- a/x-pack/test/functional/apps/dev_tools/index.ts +++ b/x-pack/test/functional/apps/dev_tools/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Dev Tools', function () { - this.tags('ciGroup13'); - loadTestFile(require.resolve('./breadcrumbs')); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./searchprofiler_editor')); diff --git a/x-pack/test/functional/apps/discover/config.ts b/x-pack/test/functional/apps/discover/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/discover/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 0a12de3fb44d6..1f4cfa15fa892 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -20,6 +20,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'security', 'share', 'spaceSelector', + 'header', ]); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -152,13 +153,17 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('allow saving currently loaded query as a copy', async () => { await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); + await queryBar.setQuery('response:404'); await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( 'ok2', 'description', true, false ); + await PageObjects.header.waitUntilLoadingHasFinished(); await savedQueryManagementComponent.savedQueryExistOrFail('ok2'); + await savedQueryManagementComponent.closeSavedQueryManagementComponent(); + await testSubjects.click('showQueryBarMenu'); await savedQueryManagementComponent.deleteSavedQuery('ok2'); }); }); diff --git a/x-pack/test/functional/apps/discover/index.ts b/x-pack/test/functional/apps/discover/index.ts index 9eda11bc6e6fb..bb5ac2f8ea9d4 100644 --- a/x-pack/test/functional/apps/discover/index.ts +++ b/x-pack/test/functional/apps/discover/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('discover', function () { - this.tags('ciGroup25'); - loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); loadTestFile(require.resolve('./async_scripted_fields')); diff --git a/x-pack/test/functional/apps/graph/config.ts b/x-pack/test/functional/apps/graph/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/graph/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/graph/index.ts b/x-pack/test/functional/apps/graph/index.ts index 561cf8e5833d4..ca0b02e8b0f7d 100644 --- a/x-pack/test/functional/apps/graph/index.ts +++ b/x-pack/test/functional/apps/graph/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('graph app', function () { - this.tags('ciGroup12'); - loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./graph')); }); diff --git a/x-pack/test/functional/apps/grok_debugger/config.ts b/x-pack/test/functional/apps/grok_debugger/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/grok_debugger/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/grok_debugger/index.ts b/x-pack/test/functional/apps/grok_debugger/index.ts index 1fed41f6e3e36..7b0bd70508b6f 100644 --- a/x-pack/test/functional/apps/grok_debugger/index.ts +++ b/x-pack/test/functional/apps/grok_debugger/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Grok Debugger App', function () { - this.tags('ciGroup13'); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/home/config.ts b/x-pack/test/functional/apps/home/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/home/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/home/index.ts b/x-pack/test/functional/apps/home/index.ts index c7579efff03a4..fd2c5b3b752c8 100644 --- a/x-pack/test/functional/apps/home/index.ts +++ b/x-pack/test/functional/apps/home/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Home page', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./feature_controls')); }); }; diff --git a/x-pack/test/functional/apps/index_lifecycle_management/config.ts b/x-pack/test/functional/apps/index_lifecycle_management/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/index_lifecycle_management/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts index d362b84ee479a..71056c2d836fc 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts @@ -29,7 +29,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { body: { type: 'fs', settings: { - // use one of the values defined in path.repo in test/functional/config.js + // use one of the values defined in path.repo in test/functional/config.base.js location: '/tmp/', }, }, diff --git a/x-pack/test/functional/apps/index_lifecycle_management/index.ts b/x-pack/test/functional/apps/index_lifecycle_management/index.ts index cf83939c942d9..38b5803bd77ef 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/index.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Index Lifecycle Management app', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); diff --git a/x-pack/test/functional/apps/index_management/config.ts b/x-pack/test/functional/apps/index_management/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/index_management/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/index_management/index.ts b/x-pack/test/functional/apps/index_management/index.ts index 81bd94769db62..83da8cc4bba89 100644 --- a/x-pack/test/functional/apps/index_management/index.ts +++ b/x-pack/test/functional/apps/index_management/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Index Management app', function () { - this.tags('ciGroup13'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); diff --git a/x-pack/test/functional/apps/infra/config.ts b/x-pack/test/functional/apps/infra/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/infra/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index 15f92b8f37fd4..d574c747bf041 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -9,14 +9,15 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('InfraOps App', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./feature_controls')); + describe('Metrics UI', function () { loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./metrics_source_configuration')); loadTestFile(require.resolve('./metrics_anomalies')); loadTestFile(require.resolve('./metrics_explorer')); }); + describe('Logs UI', function () { loadTestFile(require.resolve('./log_entry_categories_tab')); loadTestFile(require.resolve('./log_entry_rate_tab')); diff --git a/x-pack/test/functional/apps/ingest_pipelines/config.ts b/x-pack/test/functional/apps/ingest_pipelines/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/ingest_pipelines/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/ingest_pipelines/index.ts b/x-pack/test/functional/apps/ingest_pipelines/index.ts index 655fccaf35a95..3c585319cfe13 100644 --- a/x-pack/test/functional/apps/ingest_pipelines/index.ts +++ b/x-pack/test/functional/apps/ingest_pipelines/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Ingest pipelines app', function () { - this.tags('ciGroup13'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./ingest_pipelines')); }); diff --git a/x-pack/test/functional/apps/lens/README.md b/x-pack/test/functional/apps/lens/README.md new file mode 100644 index 0000000000000..5e87a8b210bdd --- /dev/null +++ b/x-pack/test/functional/apps/lens/README.md @@ -0,0 +1,7 @@ +# What are all these groups? + +These tests take a while so they have been broken up into groups with their own `config.ts` and `index.ts` file, causing each of these groups to be independent bundles of tests which can be run on some worker in CI without taking an incredible amount of time. + +Want to change the groups to something more logical? Have fun! Just make sure that each group executes on CI in less than 10 minutes or so. We don't currently have any mechanism for validating this right now, you just need to look at the times in the log output on CI, but we'll be working on tooling for making this information more accessible soon. + +- Kibana Operations \ No newline at end of file diff --git a/x-pack/test/functional/apps/lens/group1/config.ts b/x-pack/test/functional/apps/lens/group1/config.ts new file mode 100644 index 0000000000000..d927f93adeffd --- /dev/null +++ b/x-pack/test/functional/apps/lens/group1/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/group1/index.ts similarity index 55% rename from x-pack/test/functional/apps/lens/index.ts rename to x-pack/test/functional/apps/lens/group1/index.ts index 372d17b473e4d..35030e3636c96 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/group1/index.ts @@ -6,7 +6,7 @@ */ import { EsArchiver } from '@kbn/es-archiver'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext) => { const browser = getService('browser'); @@ -17,7 +17,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext const config = getService('config'); let remoteEsArchiver; - describe('lens app', () => { + describe('lens app - group 1', () => { const esArchive = 'x-pack/test/functional/es_archives/logstash_functional'; const localIndexPatternString = 'logstash-*'; const remoteIndexPatternString = 'ftr-remote:logstash-*'; @@ -70,54 +70,12 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext }); if (config.get('esTestCluster.ccs')) { - describe('', function () { - this.tags(['ciGroup3', 'skipFirefox']); - loadTestFile(require.resolve('./smokescreen')); - }); + loadTestFile(require.resolve('./smokescreen')); } else { - describe('', function () { - this.tags(['ciGroup3', 'skipFirefox']); - loadTestFile(require.resolve('./smokescreen')); - loadTestFile(require.resolve('./persistent_context')); - }); - - describe('', function () { - this.tags(['ciGroup16', 'skipFirefox']); - - loadTestFile(require.resolve('./add_to_dashboard')); - loadTestFile(require.resolve('./table_dashboard')); - loadTestFile(require.resolve('./table')); - loadTestFile(require.resolve('./runtime_fields')); - loadTestFile(require.resolve('./dashboard')); - loadTestFile(require.resolve('./multi_terms')); - loadTestFile(require.resolve('./epoch_millis')); - loadTestFile(require.resolve('./show_underlying_data')); - loadTestFile(require.resolve('./show_underlying_data_dashboard')); - }); - - describe('', function () { - this.tags(['ciGroup4', 'skipFirefox']); - - loadTestFile(require.resolve('./colors')); - loadTestFile(require.resolve('./chart_data')); - loadTestFile(require.resolve('./time_shift')); - loadTestFile(require.resolve('./drag_and_drop')); - loadTestFile(require.resolve('./disable_auto_apply')); - loadTestFile(require.resolve('./geo_field')); - loadTestFile(require.resolve('./formula')); - loadTestFile(require.resolve('./heatmap')); - loadTestFile(require.resolve('./gauge')); - loadTestFile(require.resolve('./metrics')); - loadTestFile(require.resolve('./reference_lines')); - loadTestFile(require.resolve('./annotations')); - loadTestFile(require.resolve('./inspector')); - loadTestFile(require.resolve('./error_handling')); - loadTestFile(require.resolve('./lens_tagging')); - loadTestFile(require.resolve('./lens_reporting')); - loadTestFile(require.resolve('./tsvb_open_in_lens')); - // has to be last one in the suite because it overrides saved objects - loadTestFile(require.resolve('./rollup')); - }); + loadTestFile(require.resolve('./smokescreen')); + loadTestFile(require.resolve('./persistent_context')); + loadTestFile(require.resolve('./table_dashboard')); + loadTestFile(require.resolve('./table')); } }); }; diff --git a/x-pack/test/functional/apps/lens/persistent_context.ts b/x-pack/test/functional/apps/lens/group1/persistent_context.ts similarity index 99% rename from x-pack/test/functional/apps/lens/persistent_context.ts rename to x-pack/test/functional/apps/lens/group1/persistent_context.ts index 445caa1abbec2..b6c37f8842329 100644 --- a/x-pack/test/functional/apps/lens/persistent_context.ts +++ b/x-pack/test/functional/apps/lens/group1/persistent_context.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/group1/smokescreen.ts similarity index 99% rename from x-pack/test/functional/apps/lens/smokescreen.ts rename to x-pack/test/functional/apps/lens/group1/smokescreen.ts index d817ee912664a..70887b337114f 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/group1/smokescreen.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { range } from 'lodash'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/table.ts b/x-pack/test/functional/apps/lens/group1/table.ts similarity index 99% rename from x-pack/test/functional/apps/lens/table.ts rename to x-pack/test/functional/apps/lens/group1/table.ts index 2070eb047ef61..18ecc2e90cfe4 100644 --- a/x-pack/test/functional/apps/lens/table.ts +++ b/x-pack/test/functional/apps/lens/group1/table.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/table_dashboard.ts b/x-pack/test/functional/apps/lens/group1/table_dashboard.ts similarity index 97% rename from x-pack/test/functional/apps/lens/table_dashboard.ts rename to x-pack/test/functional/apps/lens/group1/table_dashboard.ts index 6e76d816fa6a6..136f1903420de 100644 --- a/x-pack/test/functional/apps/lens/table_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group1/table_dashboard.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['lens', 'visualize', 'dashboard']); diff --git a/x-pack/test/functional/apps/lens/add_to_dashboard.ts b/x-pack/test/functional/apps/lens/group2/add_to_dashboard.ts similarity index 99% rename from x-pack/test/functional/apps/lens/add_to_dashboard.ts rename to x-pack/test/functional/apps/lens/group2/add_to_dashboard.ts index 5fbfdd0a5806e..8ad5cd41e0bec 100644 --- a/x-pack/test/functional/apps/lens/add_to_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group2/add_to_dashboard.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/x-pack/test/functional/apps/lens/group2/config.ts b/x-pack/test/functional/apps/lens/group2/config.ts new file mode 100644 index 0000000000000..d927f93adeffd --- /dev/null +++ b/x-pack/test/functional/apps/lens/group2/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/group2/dashboard.ts similarity index 98% rename from x-pack/test/functional/apps/lens/dashboard.ts rename to x-pack/test/functional/apps/lens/group2/dashboard.ts index 97dc29280761f..787a0a6a6d99a 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/group2/dashboard.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ @@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'timePicker', 'lens', 'discover', + 'unifiedSearch', ]); const find = getService('find'); @@ -163,6 +164,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickNewDashboard(); await dashboardAddPanel.clickCreateNewLink(); await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.unifiedSearch.closeTourPopoverByLocalStorage(); await PageObjects.lens.goToTimeRange(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', diff --git a/x-pack/test/functional/apps/lens/epoch_millis.ts b/x-pack/test/functional/apps/lens/group2/epoch_millis.ts similarity index 97% rename from x-pack/test/functional/apps/lens/epoch_millis.ts rename to x-pack/test/functional/apps/lens/group2/epoch_millis.ts index d882d69ddd1fd..ce773baf27ad9 100644 --- a/x-pack/test/functional/apps/lens/epoch_millis.ts +++ b/x-pack/test/functional/apps/lens/group2/epoch_millis.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/group2/index.ts b/x-pack/test/functional/apps/lens/group2/index.ts new file mode 100644 index 0000000000000..0e1c732dff41c --- /dev/null +++ b/x-pack/test/functional/apps/lens/group2/index.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 { EsArchiver } from '@kbn/es-archiver'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext) => { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['timePicker']); + const config = getService('config'); + let remoteEsArchiver; + + describe('lens app - group 2', () => { + const esArchive = 'x-pack/test/functional/es_archives/logstash_functional'; + const localIndexPatternString = 'logstash-*'; + const remoteIndexPatternString = 'ftr-remote:logstash-*'; + const localFixtures = { + lensBasic: 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json', + lensDefault: 'x-pack/test/functional/fixtures/kbn_archiver/lens/default', + }; + + const remoteFixtures = { + lensBasic: 'x-pack/test/functional/fixtures/kbn_archiver/lens/ccs/lens_basic.json', + lensDefault: 'x-pack/test/functional/fixtures/kbn_archiver/lens/ccs/default', + }; + let esNode: EsArchiver; + let fixtureDirs: { + lensBasic: string; + lensDefault: string; + }; + let indexPatternString: string; + before(async () => { + await log.debug('Starting lens before method'); + await browser.setWindowSize(1280, 1200); + try { + config.get('esTestCluster.ccs'); + remoteEsArchiver = getService('remoteEsArchiver' as 'esArchiver'); + esNode = remoteEsArchiver; + fixtureDirs = remoteFixtures; + indexPatternString = remoteIndexPatternString; + } catch (error) { + esNode = esArchiver; + fixtureDirs = localFixtures; + indexPatternString = localIndexPatternString; + } + + await esNode.load(esArchive); + // changing the timepicker default here saves us from having to set it in Discover (~8s) + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update({ + defaultIndex: indexPatternString, + 'dateFormat:tz': 'UTC', + }); + await kibanaServer.importExport.load(fixtureDirs.lensBasic); + await kibanaServer.importExport.load(fixtureDirs.lensDefault); + }); + + after(async () => { + await esArchiver.unload(esArchive); + await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.importExport.unload(fixtureDirs.lensBasic); + await kibanaServer.importExport.unload(fixtureDirs.lensDefault); + }); + + loadTestFile(require.resolve('./add_to_dashboard')); + loadTestFile(require.resolve('./runtime_fields')); + loadTestFile(require.resolve('./dashboard')); + loadTestFile(require.resolve('./multi_terms')); + loadTestFile(require.resolve('./epoch_millis')); + loadTestFile(require.resolve('./show_underlying_data')); + loadTestFile(require.resolve('./show_underlying_data_dashboard')); + }); +}; diff --git a/x-pack/test/functional/apps/lens/multi_terms.ts b/x-pack/test/functional/apps/lens/group2/multi_terms.ts similarity index 97% rename from x-pack/test/functional/apps/lens/multi_terms.ts rename to x-pack/test/functional/apps/lens/group2/multi_terms.ts index 05283b80a7c81..58fa172378964 100644 --- a/x-pack/test/functional/apps/lens/multi_terms.ts +++ b/x-pack/test/functional/apps/lens/group2/multi_terms.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/runtime_fields.ts b/x-pack/test/functional/apps/lens/group2/runtime_fields.ts similarity index 97% rename from x-pack/test/functional/apps/lens/runtime_fields.ts rename to x-pack/test/functional/apps/lens/group2/runtime_fields.ts index 252951cba4bd0..868432ce1ae4d 100644 --- a/x-pack/test/functional/apps/lens/runtime_fields.ts +++ b/x-pack/test/functional/apps/lens/group2/runtime_fields.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/show_underlying_data.ts b/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts similarity index 98% rename from x-pack/test/functional/apps/lens/show_underlying_data.ts rename to x-pack/test/functional/apps/lens/group2/show_underlying_data.ts index 4bc8be22eb8f4..bd8f02c723102 100644 --- a/x-pack/test/functional/apps/lens/show_underlying_data.ts +++ b/x-pack/test/functional/apps/lens/group2/show_underlying_data.ts @@ -5,7 +5,7 @@ * 2.0. */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header', 'discover']); @@ -87,7 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.enableFilter(); // turn off the KQL switch to change the language to lucene await testSubjects.click('indexPattern-filter-by-input > switchQueryLanguageButton'); - await testSubjects.click('languageToggle'); + await testSubjects.click('luceneLanguageMenuItem'); await testSubjects.click('indexPattern-filter-by-input > switchQueryLanguageButton'); // apparently setting a filter requires some time before and after typing to work properly await PageObjects.common.sleep(1000); diff --git a/x-pack/test/functional/apps/lens/show_underlying_data_dashboard.ts b/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts similarity index 90% rename from x-pack/test/functional/apps/lens/show_underlying_data_dashboard.ts rename to x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts index dc25e5f77f412..92e9b6fcdb58e 100644 --- a/x-pack/test/functional/apps/lens/show_underlying_data_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group2/show_underlying_data_dashboard.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; import uuid from 'uuid'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ @@ -23,6 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const filterBarService = getService('filterBar'); const queryBar = getService('queryBar'); + const savedQueryManagementComponent = getService('savedQueryManagementComponent'); const browser = getService('browser'); const retry = getService('retry'); @@ -58,8 +59,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should bring both dashboard context and visualization context to discover', async () => { await PageObjects.dashboard.switchToEditMode(); await dashboardPanelActions.clickEdit(); - + await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.switchQueryLanguage('lucene'); + await savedQueryManagementComponent.closeSavedQueryManagementComponent(); await queryBar.setQuery('host.keyword www.elastic.co'); await queryBar.submitQuery(); await filterBarService.addFilter('geo.src', 'is', 'AF'); @@ -67,8 +69,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.sleep(1000); await PageObjects.lens.saveAndReturn(); - + await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.switchQueryLanguage('kql'); + await savedQueryManagementComponent.closeSavedQueryManagementComponent(); await queryBar.setQuery('request.keyword : "/apm"'); await queryBar.submitQuery(); await filterBarService.addFilter( diff --git a/x-pack/test/functional/apps/lens/annotations.ts b/x-pack/test/functional/apps/lens/group3/annotations.ts similarity index 97% rename from x-pack/test/functional/apps/lens/annotations.ts rename to x-pack/test/functional/apps/lens/group3/annotations.ts index c54b3081f7418..2b641c6c161d4 100644 --- a/x-pack/test/functional/apps/lens/annotations.ts +++ b/x-pack/test/functional/apps/lens/group3/annotations.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/chart_data.ts b/x-pack/test/functional/apps/lens/group3/chart_data.ts similarity index 98% rename from x-pack/test/functional/apps/lens/chart_data.ts rename to x-pack/test/functional/apps/lens/group3/chart_data.ts index 7ea61c1fa3d81..6ef40c11407e8 100644 --- a/x-pack/test/functional/apps/lens/chart_data.ts +++ b/x-pack/test/functional/apps/lens/group3/chart_data.ts @@ -8,7 +8,7 @@ import { DebugState } from '@elastic/charts'; import expect from '@kbn/expect'; import { range } from 'lodash'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/colors.ts b/x-pack/test/functional/apps/lens/group3/colors.ts similarity index 96% rename from x-pack/test/functional/apps/lens/colors.ts rename to x-pack/test/functional/apps/lens/group3/colors.ts index 638da79b5c5cb..4078b0663d7ae 100644 --- a/x-pack/test/functional/apps/lens/colors.ts +++ b/x-pack/test/functional/apps/lens/group3/colors.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common']); diff --git a/x-pack/test/functional/apps/lens/group3/config.ts b/x-pack/test/functional/apps/lens/group3/config.ts new file mode 100644 index 0000000000000..d927f93adeffd --- /dev/null +++ b/x-pack/test/functional/apps/lens/group3/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/lens/disable_auto_apply.ts b/x-pack/test/functional/apps/lens/group3/disable_auto_apply.ts similarity index 96% rename from x-pack/test/functional/apps/lens/disable_auto_apply.ts rename to x-pack/test/functional/apps/lens/group3/disable_auto_apply.ts index f73ce56bae694..4ccc642dd9929 100644 --- a/x-pack/test/functional/apps/lens/disable_auto_apply.ts +++ b/x-pack/test/functional/apps/lens/group3/disable_auto_apply.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['lens', 'visualize']); @@ -50,6 +50,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.goToTimeRange(); await PageObjects.lens.disableAutoApply(); + await PageObjects.lens.closeSettingsMenu(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', diff --git a/x-pack/test/functional/apps/lens/drag_and_drop.ts b/x-pack/test/functional/apps/lens/group3/drag_and_drop.ts similarity index 99% rename from x-pack/test/functional/apps/lens/drag_and_drop.ts rename to x-pack/test/functional/apps/lens/group3/drag_and_drop.ts index d5b929481e6dc..dec72008d6f04 100644 --- a/x-pack/test/functional/apps/lens/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/group3/drag_and_drop.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/error_handling.ts b/x-pack/test/functional/apps/lens/group3/error_handling.ts similarity index 97% rename from x-pack/test/functional/apps/lens/error_handling.ts rename to x-pack/test/functional/apps/lens/group3/error_handling.ts index 87f62abf88e4f..8f6659bda1562 100644 --- a/x-pack/test/functional/apps/lens/error_handling.ts +++ b/x-pack/test/functional/apps/lens/group3/error_handling.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects([ diff --git a/x-pack/test/functional/apps/lens/formula.ts b/x-pack/test/functional/apps/lens/group3/formula.ts similarity index 99% rename from x-pack/test/functional/apps/lens/formula.ts rename to x-pack/test/functional/apps/lens/group3/formula.ts index 02d4fda0e96a6..33a24d3aefb1c 100644 --- a/x-pack/test/functional/apps/lens/formula.ts +++ b/x-pack/test/functional/apps/lens/group3/formula.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common']); diff --git a/x-pack/test/functional/apps/lens/gauge.ts b/x-pack/test/functional/apps/lens/group3/gauge.ts similarity index 98% rename from x-pack/test/functional/apps/lens/gauge.ts rename to x-pack/test/functional/apps/lens/group3/gauge.ts index c21ddf5b70791..9cbcbb606b423 100644 --- a/x-pack/test/functional/apps/lens/gauge.ts +++ b/x-pack/test/functional/apps/lens/group3/gauge.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/geo_field.ts b/x-pack/test/functional/apps/lens/group3/geo_field.ts similarity index 95% rename from x-pack/test/functional/apps/lens/geo_field.ts rename to x-pack/test/functional/apps/lens/group3/geo_field.ts index f9b8277a22731..bb8012ba50d23 100644 --- a/x-pack/test/functional/apps/lens/geo_field.ts +++ b/x-pack/test/functional/apps/lens/group3/geo_field.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'header', 'maps', 'timePicker']); diff --git a/x-pack/test/functional/apps/lens/heatmap.ts b/x-pack/test/functional/apps/lens/group3/heatmap.ts similarity index 99% rename from x-pack/test/functional/apps/lens/heatmap.ts rename to x-pack/test/functional/apps/lens/group3/heatmap.ts index ff1378ac82539..aa1e10a715547 100644 --- a/x-pack/test/functional/apps/lens/heatmap.ts +++ b/x-pack/test/functional/apps/lens/group3/heatmap.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common']); diff --git a/x-pack/test/functional/apps/lens/group3/index.ts b/x-pack/test/functional/apps/lens/group3/index.ts new file mode 100644 index 0000000000000..03c42e4c70ebf --- /dev/null +++ b/x-pack/test/functional/apps/lens/group3/index.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EsArchiver } from '@kbn/es-archiver'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext) => { + const browser = getService('browser'); + const log = getService('log'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['timePicker']); + const config = getService('config'); + let remoteEsArchiver; + + describe('lens app - group 3', () => { + const esArchive = 'x-pack/test/functional/es_archives/logstash_functional'; + const localIndexPatternString = 'logstash-*'; + const remoteIndexPatternString = 'ftr-remote:logstash-*'; + const localFixtures = { + lensBasic: 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json', + lensDefault: 'x-pack/test/functional/fixtures/kbn_archiver/lens/default', + }; + + const remoteFixtures = { + lensBasic: 'x-pack/test/functional/fixtures/kbn_archiver/lens/ccs/lens_basic.json', + lensDefault: 'x-pack/test/functional/fixtures/kbn_archiver/lens/ccs/default', + }; + let esNode: EsArchiver; + let fixtureDirs: { + lensBasic: string; + lensDefault: string; + }; + let indexPatternString: string; + before(async () => { + await log.debug('Starting lens before method'); + await browser.setWindowSize(1280, 1200); + try { + config.get('esTestCluster.ccs'); + remoteEsArchiver = getService('remoteEsArchiver' as 'esArchiver'); + esNode = remoteEsArchiver; + fixtureDirs = remoteFixtures; + indexPatternString = remoteIndexPatternString; + } catch (error) { + esNode = esArchiver; + fixtureDirs = localFixtures; + indexPatternString = localIndexPatternString; + } + + await esNode.load(esArchive); + // changing the timepicker default here saves us from having to set it in Discover (~8s) + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update({ + defaultIndex: indexPatternString, + 'dateFormat:tz': 'UTC', + }); + await kibanaServer.importExport.load(fixtureDirs.lensBasic); + await kibanaServer.importExport.load(fixtureDirs.lensDefault); + }); + + after(async () => { + await esArchiver.unload(esArchive); + await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.importExport.unload(fixtureDirs.lensBasic); + await kibanaServer.importExport.unload(fixtureDirs.lensDefault); + }); + + loadTestFile(require.resolve('./colors')); + loadTestFile(require.resolve('./chart_data')); + loadTestFile(require.resolve('./time_shift')); + loadTestFile(require.resolve('./drag_and_drop')); + loadTestFile(require.resolve('./disable_auto_apply')); + loadTestFile(require.resolve('./geo_field')); + loadTestFile(require.resolve('./formula')); + loadTestFile(require.resolve('./heatmap')); + loadTestFile(require.resolve('./gauge')); + loadTestFile(require.resolve('./metrics')); + loadTestFile(require.resolve('./reference_lines')); + loadTestFile(require.resolve('./annotations')); + loadTestFile(require.resolve('./inspector')); + loadTestFile(require.resolve('./error_handling')); + loadTestFile(require.resolve('./lens_tagging')); + loadTestFile(require.resolve('./lens_reporting')); + loadTestFile(require.resolve('./tsvb_open_in_lens')); + // has to be last one in the suite because it overrides saved objects + loadTestFile(require.resolve('./rollup')); + }); +}; diff --git a/x-pack/test/functional/apps/lens/inspector.ts b/x-pack/test/functional/apps/lens/group3/inspector.ts similarity index 96% rename from x-pack/test/functional/apps/lens/inspector.ts rename to x-pack/test/functional/apps/lens/group3/inspector.ts index d94d3413c07b0..9f52d783011c4 100644 --- a/x-pack/test/functional/apps/lens/inspector.ts +++ b/x-pack/test/functional/apps/lens/group3/inspector.ts @@ -5,7 +5,7 @@ * 2.0. */ import expect from '@kbn/expect'; -import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/lens_reporting.ts b/x-pack/test/functional/apps/lens/group3/lens_reporting.ts similarity index 96% rename from x-pack/test/functional/apps/lens/lens_reporting.ts rename to x-pack/test/functional/apps/lens/group3/lens_reporting.ts index 6dfb1dd923b3e..2cbb55ae03d97 100644 --- a/x-pack/test/functional/apps/lens/lens_reporting.ts +++ b/x-pack/test/functional/apps/lens/group3/lens_reporting.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'dashboard', 'reporting', 'timePicker']); diff --git a/x-pack/test/functional/apps/lens/lens_tagging.ts b/x-pack/test/functional/apps/lens/group3/lens_tagging.ts similarity index 96% rename from x-pack/test/functional/apps/lens/lens_tagging.ts rename to x-pack/test/functional/apps/lens/group3/lens_tagging.ts index 3852fdb0456ac..b246f84bb43ce 100644 --- a/x-pack/test/functional/apps/lens/lens_tagging.ts +++ b/x-pack/test/functional/apps/lens/group3/lens_tagging.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visualize', 'lens', 'timePicker', + 'unifiedSearch', ]); const lensTag = 'extreme-lens-tag'; @@ -36,6 +37,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.unifiedSearch.closeTourPopoverByLocalStorage(); }); after(async () => { diff --git a/x-pack/test/functional/apps/lens/metrics.ts b/x-pack/test/functional/apps/lens/group3/metrics.ts similarity index 97% rename from x-pack/test/functional/apps/lens/metrics.ts rename to x-pack/test/functional/apps/lens/group3/metrics.ts index 62a8b69141a58..bf651f7a12a1b 100644 --- a/x-pack/test/functional/apps/lens/metrics.ts +++ b/x-pack/test/functional/apps/lens/group3/metrics.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/reference_lines.ts b/x-pack/test/functional/apps/lens/group3/reference_lines.ts similarity index 98% rename from x-pack/test/functional/apps/lens/reference_lines.ts rename to x-pack/test/functional/apps/lens/group3/reference_lines.ts index 97c6ebf5b138f..f022a6cef6e7a 100644 --- a/x-pack/test/functional/apps/lens/reference_lines.ts +++ b/x-pack/test/functional/apps/lens/group3/reference_lines.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/rollup.ts b/x-pack/test/functional/apps/lens/group3/rollup.ts similarity index 98% rename from x-pack/test/functional/apps/lens/rollup.ts rename to x-pack/test/functional/apps/lens/group3/rollup.ts index 25ab766d04bbd..d42cc67ad673c 100644 --- a/x-pack/test/functional/apps/lens/rollup.ts +++ b/x-pack/test/functional/apps/lens/group3/rollup.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'header', 'timePicker']); diff --git a/x-pack/test/functional/apps/lens/time_shift.ts b/x-pack/test/functional/apps/lens/group3/time_shift.ts similarity index 97% rename from x-pack/test/functional/apps/lens/time_shift.ts rename to x-pack/test/functional/apps/lens/group3/time_shift.ts index e3363bee419aa..4dd22ea719ec7 100644 --- a/x-pack/test/functional/apps/lens/time_shift.ts +++ b/x-pack/test/functional/apps/lens/group3/time_shift.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); diff --git a/x-pack/test/functional/apps/lens/tsvb_open_in_lens.ts b/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts similarity index 99% rename from x-pack/test/functional/apps/lens/tsvb_open_in_lens.ts rename to x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts index 0315d20e5fc91..f9d21f80462e6 100644 --- a/x-pack/test/functional/apps/lens/tsvb_open_in_lens.ts +++ b/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const { visualize, visualBuilder, header, lens, timeToVisualize, dashboard, canvas } = diff --git a/x-pack/test/functional/apps/license_management/config.ts b/x-pack/test/functional/apps/license_management/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/license_management/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/license_management/index.ts b/x-pack/test/functional/apps/license_management/index.ts index a209a4370ced9..d4256588667ec 100644 --- a/x-pack/test/functional/apps/license_management/index.ts +++ b/x-pack/test/functional/apps/license_management/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('License app', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); diff --git a/x-pack/test/functional/apps/logstash/config.ts b/x-pack/test/functional/apps/logstash/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/logstash/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/logstash/feature_controls/index.ts b/x-pack/test/functional/apps/logstash/feature_controls/index.ts index eb4c681531cbc..c50611d7fa7c4 100644 --- a/x-pack/test/functional/apps/logstash/feature_controls/index.ts +++ b/x-pack/test/functional/apps/logstash/feature_controls/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('feature controls', function () { - this.tags(['ciGroup2']); - loadTestFile(require.resolve('./logstash_security')); }); } diff --git a/x-pack/test/functional/apps/logstash/index.js b/x-pack/test/functional/apps/logstash/index.js index 8c1ac233e9c57..deaf4041b1b59 100644 --- a/x-pack/test/functional/apps/logstash/index.js +++ b/x-pack/test/functional/apps/logstash/index.js @@ -7,8 +7,6 @@ export default function ({ loadTestFile }) { describe('logstash', function () { - this.tags(['ciGroup2']); - loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./pipeline_list')); loadTestFile(require.resolve('./pipeline_create')); diff --git a/x-pack/test/functional/apps/management/config.ts b/x-pack/test/functional/apps/management/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/management/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/management/feature_controls/index.ts b/x-pack/test/functional/apps/management/feature_controls/index.ts index 66eb2e4937620..17e71ef190856 100644 --- a/x-pack/test/functional/apps/management/feature_controls/index.ts +++ b/x-pack/test/functional/apps/management/feature_controls/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('feature controls', function () { - this.tags(['ciGroup2']); - loadTestFile(require.resolve('./management_security')); }); } diff --git a/x-pack/test/functional/apps/management/index.ts b/x-pack/test/functional/apps/management/index.ts index 03fec9fffe4fb..72da3e0fd739a 100644 --- a/x-pack/test/functional/apps/management/index.ts +++ b/x-pack/test/functional/apps/management/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('management', function () { - this.tags(['ciGroup2']); - loadTestFile(require.resolve('./create_index_pattern_wizard')); loadTestFile(require.resolve('./feature_controls')); }); diff --git a/x-pack/test/functional/apps/maps/README.md b/x-pack/test/functional/apps/maps/README.md new file mode 100644 index 0000000000000..5e87a8b210bdd --- /dev/null +++ b/x-pack/test/functional/apps/maps/README.md @@ -0,0 +1,7 @@ +# What are all these groups? + +These tests take a while so they have been broken up into groups with their own `config.ts` and `index.ts` file, causing each of these groups to be independent bundles of tests which can be run on some worker in CI without taking an incredible amount of time. + +Want to change the groups to something more logical? Have fun! Just make sure that each group executes on CI in less than 10 minutes or so. We don't currently have any mechanism for validating this right now, you just need to look at the times in the log output on CI, but we'll be working on tooling for making this information more accessible soon. + +- Kibana Operations \ No newline at end of file diff --git a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js b/x-pack/test/functional/apps/maps/group1/auto_fit_to_bounds.js similarity index 100% rename from x-pack/test/functional/apps/maps/auto_fit_to_bounds.js rename to x-pack/test/functional/apps/maps/group1/auto_fit_to_bounds.js diff --git a/x-pack/test/functional/apps/maps/blended_vector_layer.js b/x-pack/test/functional/apps/maps/group1/blended_vector_layer.js similarity index 100% rename from x-pack/test/functional/apps/maps/blended_vector_layer.js rename to x-pack/test/functional/apps/maps/group1/blended_vector_layer.js diff --git a/x-pack/test/functional/apps/maps/group1/config.ts b/x-pack/test/functional/apps/maps/group1/config.ts new file mode 100644 index 0000000000000..d927f93adeffd --- /dev/null +++ b/x-pack/test/functional/apps/maps/group1/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js b/x-pack/test/functional/apps/maps/group1/documents_source/docvalue_fields.js similarity index 100% rename from x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js rename to x-pack/test/functional/apps/maps/group1/documents_source/docvalue_fields.js diff --git a/x-pack/test/functional/apps/maps/documents_source/index.js b/x-pack/test/functional/apps/maps/group1/documents_source/index.js similarity index 100% rename from x-pack/test/functional/apps/maps/documents_source/index.js rename to x-pack/test/functional/apps/maps/group1/documents_source/index.js diff --git a/x-pack/test/functional/apps/maps/documents_source/search_hits.js b/x-pack/test/functional/apps/maps/group1/documents_source/search_hits.js similarity index 100% rename from x-pack/test/functional/apps/maps/documents_source/search_hits.js rename to x-pack/test/functional/apps/maps/group1/documents_source/search_hits.js diff --git a/x-pack/test/functional/apps/maps/documents_source/top_hits.js b/x-pack/test/functional/apps/maps/group1/documents_source/top_hits.js similarity index 100% rename from x-pack/test/functional/apps/maps/documents_source/top_hits.js rename to x-pack/test/functional/apps/maps/group1/documents_source/top_hits.js diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/group1/feature_controls/maps_security.ts similarity index 94% rename from x-pack/test/functional/apps/maps/feature_controls/maps_security.ts rename to x-pack/test/functional/apps/maps/group1/feature_controls/maps_security.ts index db6bfb642ebbb..94f46763acd31 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/group1/feature_controls/maps_security.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const security = getService('security'); @@ -113,18 +113,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { false ); }); - - it('allow saving currently loaded query as a copy', async () => { - await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); - await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( - 'ok2', - 'description', - true, - false - ); - await savedQueryManagementComponent.savedQueryExistOrFail('ok2'); - await savedQueryManagementComponent.deleteSavedQuery('ok2'); - }); }); describe('global maps read-only privileges', () => { diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts b/x-pack/test/functional/apps/maps/group1/feature_controls/maps_spaces.ts similarity index 98% rename from x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts rename to x-pack/test/functional/apps/maps/group1/feature_controls/maps_spaces.ts index 1beddd72d3fde..a306c15bfbdb2 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts +++ b/x-pack/test/functional/apps/maps/group1/feature_controls/maps_spaces.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { APP_ID } from '@kbn/maps-plugin/common/constants'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const spacesService = getService('spaces'); diff --git a/x-pack/test/functional/apps/maps/full_screen_mode.js b/x-pack/test/functional/apps/maps/group1/full_screen_mode.js similarity index 100% rename from x-pack/test/functional/apps/maps/full_screen_mode.js rename to x-pack/test/functional/apps/maps/group1/full_screen_mode.js diff --git a/x-pack/test/functional/apps/maps/group1/index.js b/x-pack/test/functional/apps/maps/group1/index.js new file mode 100644 index 0000000000000..be59d43012626 --- /dev/null +++ b/x-pack/test/functional/apps/maps/group1/index.js @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default function ({ loadTestFile, getService }) { + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const log = getService('log'); + const supertest = getService('supertest'); + + describe('maps app', function () { + this.tags(['skipFirefox']); + + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/maps.json' + ); + // Functional tests verify behavior when referenced index pattern saved objects can not be found. + // However, saved object import fails when reference saved objects can not be found. + // To prevent import errors, index pattern saved object references exist during import + // but are then deleted afterwards to enable testing of missing reference index pattern saved objects. + + log.info('Delete index pattern'); + log.debug('id: ' + 'idThatDoesNotExitForESGeoGridSource'); + log.debug('id: ' + 'idThatDoesNotExitForESSearchSource'); + log.debug('id: ' + 'idThatDoesNotExitForESJoinSource'); + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESGeoGridSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESSearchSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESJoinSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await esArchiver.load('x-pack/test/functional/es_archives/maps/data'); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', + }); + await browser.setWindowSize(1600, 1000); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/maps/data'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/maps.json' + ); + }); + + loadTestFile(require.resolve('./documents_source')); + loadTestFile(require.resolve('./blended_vector_layer')); + loadTestFile(require.resolve('./vector_styling')); + loadTestFile(require.resolve('./saved_object_management')); + loadTestFile(require.resolve('./sample_data')); + loadTestFile(require.resolve('./auto_fit_to_bounds')); + loadTestFile(require.resolve('./layer_visibility')); + loadTestFile(require.resolve('./feature_controls/maps_security')); + loadTestFile(require.resolve('./feature_controls/maps_spaces')); + loadTestFile(require.resolve('./full_screen_mode')); + }); +} diff --git a/x-pack/test/functional/apps/maps/layer_visibility.js b/x-pack/test/functional/apps/maps/group1/layer_visibility.js similarity index 100% rename from x-pack/test/functional/apps/maps/layer_visibility.js rename to x-pack/test/functional/apps/maps/group1/layer_visibility.js diff --git a/x-pack/test/functional/apps/maps/sample_data.js b/x-pack/test/functional/apps/maps/group1/sample_data.js similarity index 100% rename from x-pack/test/functional/apps/maps/sample_data.js rename to x-pack/test/functional/apps/maps/group1/sample_data.js diff --git a/x-pack/test/functional/apps/maps/saved_object_management.js b/x-pack/test/functional/apps/maps/group1/saved_object_management.js similarity index 100% rename from x-pack/test/functional/apps/maps/saved_object_management.js rename to x-pack/test/functional/apps/maps/group1/saved_object_management.js diff --git a/x-pack/test/functional/apps/maps/vector_styling.js b/x-pack/test/functional/apps/maps/group1/vector_styling.js similarity index 100% rename from x-pack/test/functional/apps/maps/vector_styling.js rename to x-pack/test/functional/apps/maps/group1/vector_styling.js diff --git a/x-pack/test/functional/apps/maps/group2/config.ts b/x-pack/test/functional/apps/maps/group2/config.ts new file mode 100644 index 0000000000000..d927f93adeffd --- /dev/null +++ b/x-pack/test/functional/apps/maps/group2/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/maps/embeddable/add_to_dashboard.js b/x-pack/test/functional/apps/maps/group2/embeddable/add_to_dashboard.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/add_to_dashboard.js rename to x-pack/test/functional/apps/maps/group2/embeddable/add_to_dashboard.js diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/group2/embeddable/dashboard.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/dashboard.js rename to x-pack/test/functional/apps/maps/group2/embeddable/dashboard.js diff --git a/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js b/x-pack/test/functional/apps/maps/group2/embeddable/embeddable_library.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/embeddable_library.js rename to x-pack/test/functional/apps/maps/group2/embeddable/embeddable_library.js diff --git a/x-pack/test/functional/apps/maps/embeddable/embeddable_state.js b/x-pack/test/functional/apps/maps/group2/embeddable/embeddable_state.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/embeddable_state.js rename to x-pack/test/functional/apps/maps/group2/embeddable/embeddable_state.js diff --git a/x-pack/test/functional/apps/maps/embeddable/filter_by_map_extent.js b/x-pack/test/functional/apps/maps/group2/embeddable/filter_by_map_extent.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/filter_by_map_extent.js rename to x-pack/test/functional/apps/maps/group2/embeddable/filter_by_map_extent.js diff --git a/x-pack/test/functional/apps/maps/embeddable/index.js b/x-pack/test/functional/apps/maps/group2/embeddable/index.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/index.js rename to x-pack/test/functional/apps/maps/group2/embeddable/index.js diff --git a/x-pack/test/functional/apps/maps/embeddable/save_and_return.js b/x-pack/test/functional/apps/maps/group2/embeddable/save_and_return.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/save_and_return.js rename to x-pack/test/functional/apps/maps/group2/embeddable/save_and_return.js diff --git a/x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js b/x-pack/test/functional/apps/maps/group2/embeddable/tooltip_filter_actions.js similarity index 100% rename from x-pack/test/functional/apps/maps/embeddable/tooltip_filter_actions.js rename to x-pack/test/functional/apps/maps/group2/embeddable/tooltip_filter_actions.js diff --git a/x-pack/test/functional/apps/maps/es_geo_grid_source.js b/x-pack/test/functional/apps/maps/group2/es_geo_grid_source.js similarity index 100% rename from x-pack/test/functional/apps/maps/es_geo_grid_source.js rename to x-pack/test/functional/apps/maps/group2/es_geo_grid_source.js diff --git a/x-pack/test/functional/apps/maps/group2/index.js b/x-pack/test/functional/apps/maps/group2/index.js new file mode 100644 index 0000000000000..b8b3a15a10ed5 --- /dev/null +++ b/x-pack/test/functional/apps/maps/group2/index.js @@ -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. + */ + +export default function ({ loadTestFile, getService }) { + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const log = getService('log'); + const supertest = getService('supertest'); + + describe('maps app', function () { + this.tags(['skipFirefox']); + + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/maps.json' + ); + // Functional tests verify behavior when referenced index pattern saved objects can not be found. + // However, saved object import fails when reference saved objects can not be found. + // To prevent import errors, index pattern saved object references exist during import + // but are then deleted afterwards to enable testing of missing reference index pattern saved objects. + + log.info('Delete index pattern'); + log.debug('id: ' + 'idThatDoesNotExitForESGeoGridSource'); + log.debug('id: ' + 'idThatDoesNotExitForESSearchSource'); + log.debug('id: ' + 'idThatDoesNotExitForESJoinSource'); + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESGeoGridSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESSearchSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESJoinSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await esArchiver.load('x-pack/test/functional/es_archives/maps/data'); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', + }); + await browser.setWindowSize(1600, 1000); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/maps/data'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/maps.json' + ); + }); + + loadTestFile(require.resolve('./es_geo_grid_source')); + loadTestFile(require.resolve('./embeddable')); + }); +} diff --git a/x-pack/test/functional/apps/maps/group3/config.ts b/x-pack/test/functional/apps/maps/group3/config.ts new file mode 100644 index 0000000000000..d927f93adeffd --- /dev/null +++ b/x-pack/test/functional/apps/maps/group3/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/maps/group3/index.js b/x-pack/test/functional/apps/maps/group3/index.js new file mode 100644 index 0000000000000..fda116cecc307 --- /dev/null +++ b/x-pack/test/functional/apps/maps/group3/index.js @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default function ({ loadTestFile, getService }) { + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const log = getService('log'); + const supertest = getService('supertest'); + + describe('maps app', function () { + this.tags(['skipFirefox']); + + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/maps.json' + ); + // Functional tests verify behavior when referenced index pattern saved objects can not be found. + // However, saved object import fails when reference saved objects can not be found. + // To prevent import errors, index pattern saved object references exist during import + // but are then deleted afterwards to enable testing of missing reference index pattern saved objects. + + log.info('Delete index pattern'); + log.debug('id: ' + 'idThatDoesNotExitForESGeoGridSource'); + log.debug('id: ' + 'idThatDoesNotExitForESSearchSource'); + log.debug('id: ' + 'idThatDoesNotExitForESJoinSource'); + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESGeoGridSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESSearchSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await supertest + .delete('/api/index_patterns/index_pattern/' + 'idThatDoesNotExitForESJoinSource') + .set('kbn-xsrf', 'true') + .expect(200); + + await esArchiver.load('x-pack/test/functional/es_archives/maps/data'); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', + }); + await browser.setWindowSize(1600, 1000); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/maps/data'); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/maps.json' + ); + }); + + loadTestFile(require.resolve('./reports')); + }); +} diff --git a/x-pack/test/functional/apps/maps/reports/baseline/example_map_report.png b/x-pack/test/functional/apps/maps/group3/reports/baseline/example_map_report.png similarity index 100% rename from x-pack/test/functional/apps/maps/reports/baseline/example_map_report.png rename to x-pack/test/functional/apps/maps/group3/reports/baseline/example_map_report.png diff --git a/x-pack/test/functional/apps/maps/reports/baseline/geo_map_report.png b/x-pack/test/functional/apps/maps/group3/reports/baseline/geo_map_report.png similarity index 100% rename from x-pack/test/functional/apps/maps/reports/baseline/geo_map_report.png rename to x-pack/test/functional/apps/maps/group3/reports/baseline/geo_map_report.png diff --git a/x-pack/test/functional/apps/maps/reports/index.ts b/x-pack/test/functional/apps/maps/group3/reports/index.ts similarity index 97% rename from x-pack/test/functional/apps/maps/reports/index.ts rename to x-pack/test/functional/apps/maps/group3/reports/index.ts index 4e942b1e150ef..c892842782aa6 100644 --- a/x-pack/test/functional/apps/maps/reports/index.ts +++ b/x-pack/test/functional/apps/maps/group3/reports/index.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const REPORTS_FOLDER = __dirname; diff --git a/x-pack/test/functional/apps/maps/add_layer_panel.js b/x-pack/test/functional/apps/maps/group4/add_layer_panel.js similarity index 100% rename from x-pack/test/functional/apps/maps/add_layer_panel.js rename to x-pack/test/functional/apps/maps/group4/add_layer_panel.js diff --git a/x-pack/test/functional/apps/maps/group4/config.ts b/x-pack/test/functional/apps/maps/group4/config.ts new file mode 100644 index 0000000000000..d927f93adeffd --- /dev/null +++ b/x-pack/test/functional/apps/maps/group4/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/maps/discover.js b/x-pack/test/functional/apps/maps/group4/discover.js similarity index 100% rename from x-pack/test/functional/apps/maps/discover.js rename to x-pack/test/functional/apps/maps/group4/discover.js diff --git a/x-pack/test/functional/apps/maps/es_pew_pew_source.js b/x-pack/test/functional/apps/maps/group4/es_pew_pew_source.js similarity index 100% rename from x-pack/test/functional/apps/maps/es_pew_pew_source.js rename to x-pack/test/functional/apps/maps/group4/es_pew_pew_source.js diff --git a/x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.dbf b/x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.dbf similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.dbf rename to x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.dbf diff --git a/x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.prj b/x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.prj similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.prj rename to x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.prj diff --git a/x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.shp b/x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.shp similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.shp rename to x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.shp diff --git a/x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.shx b/x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.shx similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/files/cb_2018_us_csa_500k.shx rename to x-pack/test/functional/apps/maps/group4/file_upload/files/cb_2018_us_csa_500k.shx diff --git a/x-pack/test/functional/apps/maps/file_upload/files/point.json b/x-pack/test/functional/apps/maps/group4/file_upload/files/point.json similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/files/point.json rename to x-pack/test/functional/apps/maps/group4/file_upload/files/point.json diff --git a/x-pack/test/functional/apps/maps/file_upload/files/polygon.json b/x-pack/test/functional/apps/maps/group4/file_upload/files/polygon.json similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/files/polygon.json rename to x-pack/test/functional/apps/maps/group4/file_upload/files/polygon.json diff --git a/x-pack/test/functional/apps/maps/file_upload/files/world_countries_v7.geo.json b/x-pack/test/functional/apps/maps/group4/file_upload/files/world_countries_v7.geo.json similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/files/world_countries_v7.geo.json rename to x-pack/test/functional/apps/maps/group4/file_upload/files/world_countries_v7.geo.json diff --git a/x-pack/test/functional/apps/maps/file_upload/geojson.js b/x-pack/test/functional/apps/maps/group4/file_upload/geojson.js similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/geojson.js rename to x-pack/test/functional/apps/maps/group4/file_upload/geojson.js diff --git a/x-pack/test/functional/apps/maps/file_upload/index.js b/x-pack/test/functional/apps/maps/group4/file_upload/index.js similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/index.js rename to x-pack/test/functional/apps/maps/group4/file_upload/index.js diff --git a/x-pack/test/functional/apps/maps/file_upload/shapefile.js b/x-pack/test/functional/apps/maps/group4/file_upload/shapefile.js similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/shapefile.js rename to x-pack/test/functional/apps/maps/group4/file_upload/shapefile.js diff --git a/x-pack/test/functional/apps/maps/file_upload/wizard.js b/x-pack/test/functional/apps/maps/group4/file_upload/wizard.js similarity index 100% rename from x-pack/test/functional/apps/maps/file_upload/wizard.js rename to x-pack/test/functional/apps/maps/group4/file_upload/wizard.js diff --git a/x-pack/test/functional/apps/maps/geofile_wizard_auto_open.ts b/x-pack/test/functional/apps/maps/group4/geofile_wizard_auto_open.ts similarity index 95% rename from x-pack/test/functional/apps/maps/geofile_wizard_auto_open.ts rename to x-pack/test/functional/apps/maps/group4/geofile_wizard_auto_open.ts index dd69cc7882fc7..ebe434b6afe6e 100644 --- a/x-pack/test/functional/apps/maps/geofile_wizard_auto_open.ts +++ b/x-pack/test/functional/apps/maps/group4/geofile_wizard_auto_open.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'maps']); diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/group4/index.js similarity index 57% rename from x-pack/test/functional/apps/maps/index.js rename to x-pack/test/functional/apps/maps/group4/index.js index 9d7e7a9cbddcc..70cb5ac660768 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/group4/index.js @@ -58,46 +58,18 @@ export default function ({ loadTestFile, getService }) { ); }); - describe('', async function () { - this.tags('ciGroup9'); - loadTestFile(require.resolve('./documents_source')); - loadTestFile(require.resolve('./blended_vector_layer')); - loadTestFile(require.resolve('./vector_styling')); - loadTestFile(require.resolve('./saved_object_management')); - loadTestFile(require.resolve('./sample_data')); - loadTestFile(require.resolve('./auto_fit_to_bounds')); - loadTestFile(require.resolve('./layer_visibility')); - loadTestFile(require.resolve('./feature_controls/maps_security')); - loadTestFile(require.resolve('./feature_controls/maps_spaces')); - loadTestFile(require.resolve('./full_screen_mode')); - }); - - describe('', function () { - this.tags('ciGroup22'); - loadTestFile(require.resolve('./es_geo_grid_source')); - loadTestFile(require.resolve('./embeddable')); - }); - - describe('', function () { - this.tags('ciGroup2'); // same group used in x-pack/test/reporting_functional - loadTestFile(require.resolve('./reports')); - }); - - describe('', function () { - this.tags('ciGroup10'); - loadTestFile(require.resolve('./es_pew_pew_source')); - loadTestFile(require.resolve('./joins')); - loadTestFile(require.resolve('./mvt_joins')); - loadTestFile(require.resolve('./mapbox_styles')); - loadTestFile(require.resolve('./mvt_scaling')); - loadTestFile(require.resolve('./mvt_geotile_grid')); - loadTestFile(require.resolve('./add_layer_panel')); - loadTestFile(require.resolve('./file_upload')); - loadTestFile(require.resolve('./layer_errors')); - loadTestFile(require.resolve('./visualize_create_menu')); - loadTestFile(require.resolve('./discover')); - loadTestFile(require.resolve('./geofile_wizard_auto_open')); - loadTestFile(require.resolve('./lens')); - }); + loadTestFile(require.resolve('./es_pew_pew_source')); + loadTestFile(require.resolve('./joins')); + loadTestFile(require.resolve('./mvt_joins')); + loadTestFile(require.resolve('./mapbox_styles')); + loadTestFile(require.resolve('./mvt_scaling')); + loadTestFile(require.resolve('./mvt_geotile_grid')); + loadTestFile(require.resolve('./add_layer_panel')); + loadTestFile(require.resolve('./file_upload')); + loadTestFile(require.resolve('./layer_errors')); + loadTestFile(require.resolve('./visualize_create_menu')); + loadTestFile(require.resolve('./discover')); + loadTestFile(require.resolve('./geofile_wizard_auto_open')); + loadTestFile(require.resolve('./lens')); }); } diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/group4/joins.js similarity index 100% rename from x-pack/test/functional/apps/maps/joins.js rename to x-pack/test/functional/apps/maps/group4/joins.js diff --git a/x-pack/test/functional/apps/maps/layer_errors.js b/x-pack/test/functional/apps/maps/group4/layer_errors.js similarity index 100% rename from x-pack/test/functional/apps/maps/layer_errors.js rename to x-pack/test/functional/apps/maps/group4/layer_errors.js diff --git a/x-pack/test/functional/apps/maps/lens/choropleth_chart.ts b/x-pack/test/functional/apps/maps/group4/lens/choropleth_chart.ts similarity index 97% rename from x-pack/test/functional/apps/maps/lens/choropleth_chart.ts rename to x-pack/test/functional/apps/maps/group4/lens/choropleth_chart.ts index 420f895fe6aa6..b026dd7444748 100644 --- a/x-pack/test/functional/apps/maps/lens/choropleth_chart.ts +++ b/x-pack/test/functional/apps/maps/group4/lens/choropleth_chart.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'maps']); diff --git a/x-pack/test/functional/apps/maps/lens/index.ts b/x-pack/test/functional/apps/maps/group4/lens/index.ts similarity index 85% rename from x-pack/test/functional/apps/maps/lens/index.ts rename to x-pack/test/functional/apps/maps/group4/lens/index.ts index 78086303166a1..484faf431be75 100644 --- a/x-pack/test/functional/apps/maps/lens/index.ts +++ b/x-pack/test/functional/apps/maps/group4/lens/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('lens', function () { diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/group4/mapbox_styles.js similarity index 100% rename from x-pack/test/functional/apps/maps/mapbox_styles.js rename to x-pack/test/functional/apps/maps/group4/mapbox_styles.js diff --git a/x-pack/test/functional/apps/maps/mvt_geotile_grid.js b/x-pack/test/functional/apps/maps/group4/mvt_geotile_grid.js similarity index 100% rename from x-pack/test/functional/apps/maps/mvt_geotile_grid.js rename to x-pack/test/functional/apps/maps/group4/mvt_geotile_grid.js diff --git a/x-pack/test/functional/apps/maps/mvt_joins.ts b/x-pack/test/functional/apps/maps/group4/mvt_joins.ts similarity index 98% rename from x-pack/test/functional/apps/maps/mvt_joins.ts rename to x-pack/test/functional/apps/maps/group4/mvt_joins.ts index 2ae8f7ea5943b..c6567c84b2165 100644 --- a/x-pack/test/functional/apps/maps/mvt_joins.ts +++ b/x-pack/test/functional/apps/maps/group4/mvt_joins.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['maps']); diff --git a/x-pack/test/functional/apps/maps/mvt_scaling.js b/x-pack/test/functional/apps/maps/group4/mvt_scaling.js similarity index 100% rename from x-pack/test/functional/apps/maps/mvt_scaling.js rename to x-pack/test/functional/apps/maps/group4/mvt_scaling.js diff --git a/x-pack/test/functional/apps/maps/visualize_create_menu.js b/x-pack/test/functional/apps/maps/group4/visualize_create_menu.js similarity index 100% rename from x-pack/test/functional/apps/maps/visualize_create_menu.js rename to x-pack/test/functional/apps/maps/group4/visualize_create_menu.js diff --git a/x-pack/test/functional/apps/ml/README.md b/x-pack/test/functional/apps/ml/README.md new file mode 100644 index 0000000000000..5e87a8b210bdd --- /dev/null +++ b/x-pack/test/functional/apps/ml/README.md @@ -0,0 +1,7 @@ +# What are all these groups? + +These tests take a while so they have been broken up into groups with their own `config.ts` and `index.ts` file, causing each of these groups to be independent bundles of tests which can be run on some worker in CI without taking an incredible amount of time. + +Want to change the groups to something more logical? Have fun! Just make sure that each group executes on CI in less than 10 minutes or so. We don't currently have any mechanism for validating this right now, you just need to look at the times in the log output on CI, but we'll be working on tooling for making this information more accessible soon. + +- Kibana Operations \ No newline at end of file diff --git a/x-pack/test/functional/apps/ml/data_visualizer/config.ts b/x-pack/test/functional/apps/ml/data_visualizer/config.ts new file mode 100644 index 0000000000000..d927f93adeffd --- /dev/null +++ b/x-pack/test/functional/apps/ml/data_visualizer/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts index f5610e8b607da..ef15775f86204 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts @@ -5,8 +5,6 @@ * 2.0. */ -import path from 'path'; - import { ML_JOB_FIELD_TYPES } from '@kbn/ml-plugin/common/constants/field_types'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -16,7 +14,7 @@ export default function ({ getService }: FtrProviderContext) { const testDataListPositive = [ { suiteSuffix: 'with an artificial server log', - filePath: path.join(__dirname, 'files_to_import', 'artificial_server_log'), + filePath: require.resolve('./files_to_import/artificial_server_log'), indexName: 'user-import_1', createIndexPattern: false, fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER, ML_JOB_FIELD_TYPES.DATE], @@ -116,7 +114,7 @@ export default function ({ getService }: FtrProviderContext) { }, { suiteSuffix: 'with a file containing geo field', - filePath: path.join(__dirname, 'files_to_import', 'geo_file.csv'), + filePath: require.resolve('./files_to_import/geo_file.csv'), indexName: 'user-import_2', createIndexPattern: false, fieldTypeFilters: [ML_JOB_FIELD_TYPES.GEO_POINT], @@ -158,7 +156,7 @@ export default function ({ getService }: FtrProviderContext) { }, { suiteSuffix: 'with a file with a missing new line char at the end', - filePath: path.join(__dirname, 'files_to_import', 'missing_end_of_file_newline.csv'), + filePath: require.resolve('./files_to_import/missing_end_of_file_newline.csv'), indexName: 'user-import_3', createIndexPattern: false, fieldTypeFilters: [], @@ -205,7 +203,7 @@ export default function ({ getService }: FtrProviderContext) { const testDataListNegative = [ { suiteSuffix: 'with a non-log file', - filePath: path.join(__dirname, 'files_to_import', 'not_a_log_file'), + filePath: require.resolve('./files_to_import/not_a_log_file'), }, ]; diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index.ts b/x-pack/test/functional/apps/ml/data_visualizer/index.ts index 3bb8e3d728318..973ebf2bbe3ab 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index.ts @@ -7,10 +7,38 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; -export default function ({ loadTestFile }: FtrProviderContext) { - describe('data visualizer', function () { +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('machine learning - data visualizer', function () { this.tags(['skipFirefox', 'mlqa']); + before(async () => { + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote_small'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization_small'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/event_rate_nanos'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/bm_classification'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ihp_outlier'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/egs_regression'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); + + await ml.testResources.resetKibanaTimeZone(); + }); + loadTestFile(require.resolve('./index_data_visualizer')); loadTestFile(require.resolve('./index_data_visualizer_grid_in_discover')); loadTestFile(require.resolve('./index_data_visualizer_grid_in_dashboard')); diff --git a/x-pack/test/functional/apps/ml/group1/config.ts b/x-pack/test/functional/apps/ml/group1/config.ts new file mode 100644 index 0000000000000..d927f93adeffd --- /dev/null +++ b/x-pack/test/functional/apps/ml/group1/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation.ts similarity index 98% rename from x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation.ts index 2ba4ac6f08350..0cf7c4177f057 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation_saved_search.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation_saved_search.ts similarity index 98% rename from x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation_saved_search.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation_saved_search.ts index 67550ae17a4b0..cfba10c25b17b 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation_saved_search.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation_saved_search.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/cloning.ts similarity index 99% rename from x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/cloning.ts index 3a33c95edba42..82f76e66b4ebd 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/cloning.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { DeepPartial } from '@kbn/ml-plugin/common/types/common'; import { DataFrameAnalyticsConfig } from '@kbn/ml-plugin/public/application/data_frame_analytics/common'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/index.ts similarity index 93% rename from x-pack/test/functional/apps/ml/data_frame_analytics/index.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/index.ts index bc11a44148546..cf9bd17f11b81 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('data frame analytics', function () { diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation.ts similarity index 97% rename from x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation.ts index 1dacd8a7e80b4..e9146ce548422 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -160,12 +160,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the source data preview'); await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewExists(); - - await ml.testExecution.logTestStep('enables the source data preview histogram charts'); - await ml.dataFrameAnalyticsCreation.enableSourceDataPreviewHistogramCharts(true); + await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewHistogramChartEnabled(true); await ml.testExecution.logTestStep('displays the source data preview histogram charts'); - await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewHistogramCharts( + await ml.dataFrameAnalyticsCreation.enableAndAssertSourceDataPreviewHistogramCharts( testData.expected.histogramCharts ); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation_saved_search.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation_saved_search.ts similarity index 97% rename from x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation_saved_search.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation_saved_search.ts index 861be18591a11..1e428531e6aa9 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation_saved_search.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation_saved_search.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -217,12 +217,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the source data preview'); await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewExists(); - - await ml.testExecution.logTestStep('enables the source data preview histogram charts'); - await ml.dataFrameAnalyticsCreation.enableSourceDataPreviewHistogramCharts(true); + await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewHistogramChartEnabled(true); await ml.testExecution.logTestStep('displays the source data preview histogram charts'); - await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewHistogramCharts( + await ml.dataFrameAnalyticsCreation.enableAndAssertSourceDataPreviewHistogramCharts( testData.expected.histogramCharts ); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation.ts similarity index 98% rename from x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation.ts index 7a84c41aa4a66..a0cbd123b5169 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation_saved_search.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation_saved_search.ts similarity index 98% rename from x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation_saved_search.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation_saved_search.ts index e22c4908486d1..6b09b35c610a0 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation_saved_search.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation_saved_search.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/results_view_content.ts similarity index 99% rename from x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts rename to x-pack/test/functional/apps/ml/group1/data_frame_analytics/results_view_content.ts index 2bddf0a7d9512..8d04c4897dab0 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts +++ b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/results_view_content.ts @@ -8,7 +8,7 @@ import { DeepPartial } from '@kbn/ml-plugin/common/types/common'; import { DataFrameAnalyticsConfig } from '@kbn/ml-plugin/public/application/data_frame_analytics/common'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/index.ts b/x-pack/test/functional/apps/ml/group1/index.ts similarity index 64% rename from x-pack/test/functional/apps/ml/index.ts rename to x-pack/test/functional/apps/ml/group1/index.ts index c58b20e1c374b..7129f3e24d4f1 100644 --- a/x-pack/test/functional/apps/ml/index.ts +++ b/x-pack/test/functional/apps/ml/group1/index.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('machine learning', function () { + describe('machine learning - group 2', () => { before(async () => { await ml.securityCommon.createMlRoles(); await ml.securityCommon.createMlUsers(); @@ -37,26 +37,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.resetKibanaTimeZone(); }); - describe('', function () { - this.tags('ciGroup15'); - loadTestFile(require.resolve('./permissions')); - loadTestFile(require.resolve('./pages')); - loadTestFile(require.resolve('./data_visualizer')); - loadTestFile(require.resolve('./data_frame_analytics')); - loadTestFile(require.resolve('./model_management')); - }); - - describe('', function () { - this.tags('ciGroup26'); - loadTestFile(require.resolve('./anomaly_detection')); - }); - - describe('', function () { - this.tags('ciGroup8'); - loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./settings')); - loadTestFile(require.resolve('./embeddables')); - loadTestFile(require.resolve('./stack_management_jobs')); - }); + loadTestFile(require.resolve('./permissions')); + loadTestFile(require.resolve('./pages')); + loadTestFile(require.resolve('./data_frame_analytics')); + loadTestFile(require.resolve('./model_management')); }); } diff --git a/x-pack/test/functional/apps/ml/model_management/index.ts b/x-pack/test/functional/apps/ml/group1/model_management/index.ts similarity index 86% rename from x-pack/test/functional/apps/ml/model_management/index.ts rename to x-pack/test/functional/apps/ml/group1/model_management/index.ts index e958392d9ba74..5595486260dee 100644 --- a/x-pack/test/functional/apps/ml/model_management/index.ts +++ b/x-pack/test/functional/apps/ml/group1/model_management/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('model management', function () { diff --git a/x-pack/test/functional/apps/ml/model_management/model_list.ts b/x-pack/test/functional/apps/ml/group1/model_management/model_list.ts similarity index 99% rename from x-pack/test/functional/apps/ml/model_management/model_list.ts rename to x-pack/test/functional/apps/ml/group1/model_management/model_list.ts index 08fb3b7124aec..ca360130b89f9 100644 --- a/x-pack/test/functional/apps/ml/model_management/model_list.ts +++ b/x-pack/test/functional/apps/ml/group1/model_management/model_list.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); diff --git a/x-pack/test/functional/apps/ml/pages.ts b/x-pack/test/functional/apps/ml/group1/pages.ts similarity index 94% rename from x-pack/test/functional/apps/ml/pages.ts rename to x-pack/test/functional/apps/ml/group1/pages.ts index 6c8687d213cee..2cc271e67194e 100644 --- a/x-pack/test/functional/apps/ml/pages.ts +++ b/x-pack/test/functional/apps/ml/group1/pages.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); @@ -21,7 +21,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('loads the ML home page'); await ml.navigation.navigateToMl(); - await ml.testExecution.logTestStep('loads the overview page'); + await ml.testExecution.logTestStep('loads the overview page'); await ml.navigation.navigateToOverview(); await ml.testExecution.logTestStep('loads the anomaly detection area'); diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/group1/permissions/full_ml_access.ts similarity index 98% rename from x-pack/test/functional/apps/ml/permissions/full_ml_access.ts rename to x-pack/test/functional/apps/ml/group1/permissions/full_ml_access.ts index 467bfc370e8e2..c632ae48b3f88 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/group1/permissions/full_ml_access.ts @@ -5,11 +5,9 @@ * 2.0. */ -import path from 'path'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { FtrProviderContext } from '../../../ftr_provider_context'; - -import { USER } from '../../../services/ml/security_common'; +import { USER } from '../../../../services/ml/security_common'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -123,12 +121,8 @@ export default function ({ getService }: FtrProviderContext) { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - const uploadFilePath = path.join( - __dirname, - '..', - 'data_visualizer', - 'files_to_import', - 'artificial_server_log' + const uploadFilePath = require.resolve( + '../../data_visualizer/files_to_import/artificial_server_log' ); const expectedUploadFileTitle = 'artificial_server_log'; diff --git a/x-pack/test/functional/apps/ml/permissions/index.ts b/x-pack/test/functional/apps/ml/group1/permissions/index.ts similarity index 88% rename from x-pack/test/functional/apps/ml/permissions/index.ts rename to x-pack/test/functional/apps/ml/group1/permissions/index.ts index e777f241eaf85..23d7d6fe9e2b5 100644 --- a/x-pack/test/functional/apps/ml/permissions/index.ts +++ b/x-pack/test/functional/apps/ml/group1/permissions/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('permissions', function () { diff --git a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional/apps/ml/group1/permissions/no_ml_access.ts similarity index 93% rename from x-pack/test/functional/apps/ml/permissions/no_ml_access.ts rename to x-pack/test/functional/apps/ml/group1/permissions/no_ml_access.ts index 6132e6e63b1b0..4a1c108b2fa5a 100644 --- a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional/apps/ml/group1/permissions/no_ml_access.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { USER } from '../../../services/ml/security_common'; +import { USER } from '../../../../services/ml/security_common'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'error']); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/group1/permissions/read_ml_access.ts similarity index 98% rename from x-pack/test/functional/apps/ml/permissions/read_ml_access.ts rename to x-pack/test/functional/apps/ml/group1/permissions/read_ml_access.ts index fd9cb2cb4c79e..a18a6075055a6 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/group1/permissions/read_ml_access.ts @@ -5,11 +5,9 @@ * 2.0. */ -import path from 'path'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { FtrProviderContext } from '../../../ftr_provider_context'; - -import { USER } from '../../../services/ml/security_common'; +import { USER } from '../../../../services/ml/security_common'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -117,12 +115,8 @@ export default function ({ getService }: FtrProviderContext) { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - const uploadFilePath = path.join( - __dirname, - '..', - 'data_visualizer', - 'files_to_import', - 'artificial_server_log' + const uploadFilePath = require.resolve( + '../../data_visualizer/files_to_import/artificial_server_log' ); const expectedUploadFileTitle = 'artificial_server_log'; diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/advanced_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/advanced_job.ts index 1fc4c87619f18..ba0d030cfcf6f 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/advanced_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; interface Detector { identifier: string; diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/aggregated_scripted_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/aggregated_scripted_job.ts index c47171c1cd75a..78974ecf1e64c 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/aggregated_scripted_job.ts @@ -6,7 +6,7 @@ */ import { Datafeed, Job } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/annotations.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/annotations.ts index 3b36701e651a7..17c576281835a 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/annotations.ts @@ -6,7 +6,7 @@ */ import { Annotation } from '@kbn/ml-plugin/common/types/annotations'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/anomaly_explorer.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/anomaly_explorer.ts index bf6fcd10a9152..c71f4a5789fd2 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/anomaly_explorer.ts @@ -6,7 +6,7 @@ */ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; // @ts-expect-error not full interface const JOB_CONFIG: Job = { diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/categorization_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/categorization_job.ts index 555040ef8e59e..96c02f7827a58 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/categorization_job.ts @@ -6,7 +6,7 @@ */ import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '@kbn/ml-plugin/common/constants/categorization_job'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/custom_urls.ts similarity index 98% rename from x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/custom_urls.ts index a83f5d814c028..1e6e020aff69c 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/custom_urls.ts @@ -7,12 +7,12 @@ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; import { TIME_RANGE_TYPE } from '@kbn/ml-plugin/public/application/jobs/components/custom_url_editor/constants'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import type { DiscoverUrlConfig, DashboardUrlConfig, OtherUrlConfig, -} from '../../../services/ml/job_table'; +} from '../../../../services/ml/job_table'; // @ts-expect-error doesn't implement the full interface const JOB_CONFIG: Job = { diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/date_nanos_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/date_nanos_job.ts index 0401d13780403..4b593aacbebf1 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/date_nanos_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; interface Detector { identifier: string; diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/forecasts.ts similarity index 98% rename from x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/forecasts.ts index 964e0762d9322..b290789419ed8 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/forecasts.ts @@ -6,7 +6,7 @@ */ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; // @ts-expect-error not full interface const JOB_CONFIG: Job = { diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/index.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/index.ts similarity index 94% rename from x-pack/test/functional/apps/ml/anomaly_detection/index.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/index.ts index ed5f618f86644..a1127c0e71c77 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('anomaly detection', function () { diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/multi_metric_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/multi_metric_job.ts index 4786f51bdc414..783312b0d8608 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/multi_metric_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/population_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/population_job.ts index f522f5ebefd9a..af2573e21f93d 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/population_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/saved_search_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/saved_search_job.ts index f314052035ff1..72dbac602cf8f 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/saved_search_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job.ts index 8ec2e4a48e228..e698dd270e1a8 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job_without_datafeed_start.ts similarity index 98% rename from x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job_without_datafeed_start.ts index 8d08fb84a8f55..2afa284fcc3d7 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job_without_datafeed_start.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts b/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_viewer.ts similarity index 99% rename from x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts rename to x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_viewer.ts index c0946713a2564..b970a0efe5602 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts +++ b/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_viewer.ts @@ -6,7 +6,7 @@ */ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; // @ts-expect-error not full interface const JOB_CONFIG: Job = { diff --git a/x-pack/test/functional/apps/ml/group2/config.ts b/x-pack/test/functional/apps/ml/group2/config.ts new file mode 100644 index 0000000000000..d927f93adeffd --- /dev/null +++ b/x-pack/test/functional/apps/ml/group2/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/ml/group2/index.ts b/x-pack/test/functional/apps/ml/group2/index.ts new file mode 100644 index 0000000000000..4515715327e05 --- /dev/null +++ b/x-pack/test/functional/apps/ml/group2/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('machine learning - group 2', () => { + before(async () => { + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote_small'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization_small'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/event_rate_nanos'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/bm_classification'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ihp_outlier'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/egs_regression'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); + + await ml.testResources.resetKibanaTimeZone(); + }); + + loadTestFile(require.resolve('./anomaly_detection')); + }); +} diff --git a/x-pack/test/functional/apps/ml/group3/config.ts b/x-pack/test/functional/apps/ml/group3/config.ts new file mode 100644 index 0000000000000..d927f93adeffd --- /dev/null +++ b/x-pack/test/functional/apps/ml/group3/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts b/x-pack/test/functional/apps/ml/group3/embeddables/anomaly_charts_dashboard_embeddables.ts similarity index 98% rename from x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts rename to x-pack/test/functional/apps/ml/group3/embeddables/anomaly_charts_dashboard_embeddables.ts index 89b733c21498f..68981de99fc9a 100644 --- a/x-pack/test/functional/apps/ml/embeddables/anomaly_charts_dashboard_embeddables.ts +++ b/x-pack/test/functional/apps/ml/group3/embeddables/anomaly_charts_dashboard_embeddables.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { JOB_CONFIG, DATAFEED_CONFIG, ML_EMBEDDABLE_TYPES } from './constants'; const testDataList = [ diff --git a/x-pack/test/functional/apps/ml/embeddables/anomaly_embeddables_migration.ts b/x-pack/test/functional/apps/ml/group3/embeddables/anomaly_embeddables_migration.ts similarity index 98% rename from x-pack/test/functional/apps/ml/embeddables/anomaly_embeddables_migration.ts rename to x-pack/test/functional/apps/ml/group3/embeddables/anomaly_embeddables_migration.ts index ed38ff7021a92..a4c50549f5aed 100644 --- a/x-pack/test/functional/apps/ml/embeddables/anomaly_embeddables_migration.ts +++ b/x-pack/test/functional/apps/ml/group3/embeddables/anomaly_embeddables_migration.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { JOB_CONFIG, DATAFEED_CONFIG, ML_EMBEDDABLE_TYPES } from './constants'; const testDataList = [ diff --git a/x-pack/test/functional/apps/ml/embeddables/constants.ts b/x-pack/test/functional/apps/ml/group3/embeddables/constants.ts similarity index 100% rename from x-pack/test/functional/apps/ml/embeddables/constants.ts rename to x-pack/test/functional/apps/ml/group3/embeddables/constants.ts diff --git a/x-pack/test/functional/apps/ml/embeddables/index.ts b/x-pack/test/functional/apps/ml/group3/embeddables/index.ts similarity index 88% rename from x-pack/test/functional/apps/ml/embeddables/index.ts rename to x-pack/test/functional/apps/ml/group3/embeddables/index.ts index 31074a59866a6..d786491e55a4e 100644 --- a/x-pack/test/functional/apps/ml/embeddables/index.ts +++ b/x-pack/test/functional/apps/ml/group3/embeddables/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('embeddables', function () { diff --git a/x-pack/test/functional/apps/ml/feature_controls/index.ts b/x-pack/test/functional/apps/ml/group3/feature_controls/index.ts similarity index 87% rename from x-pack/test/functional/apps/ml/feature_controls/index.ts rename to x-pack/test/functional/apps/ml/group3/feature_controls/index.ts index ffe419b506fd6..ab0988c424761 100644 --- a/x-pack/test/functional/apps/ml/feature_controls/index.ts +++ b/x-pack/test/functional/apps/ml/group3/feature_controls/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('feature controls', function () { diff --git a/x-pack/test/functional/apps/ml/feature_controls/ml_security.ts b/x-pack/test/functional/apps/ml/group3/feature_controls/ml_security.ts similarity index 98% rename from x-pack/test/functional/apps/ml/feature_controls/ml_security.ts rename to x-pack/test/functional/apps/ml/group3/feature_controls/ml_security.ts index 58af3abbf6a47..fd498f00a8262 100644 --- a/x-pack/test/functional/apps/ml/feature_controls/ml_security.ts +++ b/x-pack/test/functional/apps/ml/group3/feature_controls/ml_security.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const security = getService('security'); diff --git a/x-pack/test/functional/apps/ml/feature_controls/ml_spaces.ts b/x-pack/test/functional/apps/ml/group3/feature_controls/ml_spaces.ts similarity index 97% rename from x-pack/test/functional/apps/ml/feature_controls/ml_spaces.ts rename to x-pack/test/functional/apps/ml/group3/feature_controls/ml_spaces.ts index 1ea0e1e717e5f..0352a0059ba55 100644 --- a/x-pack/test/functional/apps/ml/feature_controls/ml_spaces.ts +++ b/x-pack/test/functional/apps/ml/group3/feature_controls/ml_spaces.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const spacesService = getService('spaces'); diff --git a/x-pack/test/functional/apps/ml/group3/index.ts b/x-pack/test/functional/apps/ml/group3/index.ts new file mode 100644 index 0000000000000..e85b95b274720 --- /dev/null +++ b/x-pack/test/functional/apps/ml/group3/index.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('machine learning - group 3', function () { + before(async () => { + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote_small'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization_small'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/event_rate_nanos'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/bm_classification'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ihp_outlier'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/egs_regression'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); + + await ml.testResources.resetKibanaTimeZone(); + }); + + loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./settings')); + loadTestFile(require.resolve('./embeddables')); + loadTestFile(require.resolve('./stack_management_jobs')); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/calendar_creation.ts b/x-pack/test/functional/apps/ml/group3/settings/calendar_creation.ts similarity index 98% rename from x-pack/test/functional/apps/ml/settings/calendar_creation.ts rename to x-pack/test/functional/apps/ml/group3/settings/calendar_creation.ts index f0f5cd71cafe7..91121528d477a 100644 --- a/x-pack/test/functional/apps/ml/settings/calendar_creation.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/calendar_creation.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { asyncForEach, createJobConfig } from './common'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/functional/apps/ml/settings/calendar_delete.ts b/x-pack/test/functional/apps/ml/group3/settings/calendar_delete.ts similarity index 96% rename from x-pack/test/functional/apps/ml/settings/calendar_delete.ts rename to x-pack/test/functional/apps/ml/group3/settings/calendar_delete.ts index ff417a32c1d7c..28b526147c96e 100644 --- a/x-pack/test/functional/apps/ml/settings/calendar_delete.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/calendar_delete.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { asyncForEach } from './common'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/functional/apps/ml/settings/calendar_edit.ts b/x-pack/test/functional/apps/ml/group3/settings/calendar_edit.ts similarity index 98% rename from x-pack/test/functional/apps/ml/settings/calendar_edit.ts rename to x-pack/test/functional/apps/ml/group3/settings/calendar_edit.ts index 70c3e50ec309b..9f68ccc8a1196 100644 --- a/x-pack/test/functional/apps/ml/settings/calendar_edit.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/calendar_edit.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { asyncForEach, createJobConfig } from './common'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/functional/apps/ml/settings/common.ts b/x-pack/test/functional/apps/ml/group3/settings/common.ts similarity index 89% rename from x-pack/test/functional/apps/ml/settings/common.ts rename to x-pack/test/functional/apps/ml/group3/settings/common.ts index f161ae637e3ad..77d798ecf241b 100644 --- a/x-pack/test/functional/apps/ml/settings/common.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/common.ts @@ -5,7 +5,10 @@ * 2.0. */ -export async function asyncForEach(array: T[], callback: (item: T, index: number) => void) { +export async function asyncForEach( + array: T[], + callback: (item: T, index: number) => Promise +) { for (let index = 0; index < array.length; index++) { await callback(array[index], index); } diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_creation.ts b/x-pack/test/functional/apps/ml/group3/settings/filter_list_creation.ts similarity index 96% rename from x-pack/test/functional/apps/ml/settings/filter_list_creation.ts rename to x-pack/test/functional/apps/ml/group3/settings/filter_list_creation.ts index 3938b73a131f4..38ee8a3e6e4c2 100644 --- a/x-pack/test/functional/apps/ml/settings/filter_list_creation.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/filter_list_creation.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_delete.ts b/x-pack/test/functional/apps/ml/group3/settings/filter_list_delete.ts similarity index 96% rename from x-pack/test/functional/apps/ml/settings/filter_list_delete.ts rename to x-pack/test/functional/apps/ml/group3/settings/filter_list_delete.ts index aad56ffe55606..cdbf26ea12a03 100644 --- a/x-pack/test/functional/apps/ml/settings/filter_list_delete.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/filter_list_delete.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { asyncForEach } from './common'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_edit.ts b/x-pack/test/functional/apps/ml/group3/settings/filter_list_edit.ts similarity index 97% rename from x-pack/test/functional/apps/ml/settings/filter_list_edit.ts rename to x-pack/test/functional/apps/ml/group3/settings/filter_list_edit.ts index 7acd1b2bbc123..0a4c4f63ee741 100644 --- a/x-pack/test/functional/apps/ml/settings/filter_list_edit.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/filter_list_edit.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { asyncForEach } from './common'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/functional/apps/ml/settings/index.ts b/x-pack/test/functional/apps/ml/group3/settings/index.ts similarity index 91% rename from x-pack/test/functional/apps/ml/settings/index.ts rename to x-pack/test/functional/apps/ml/group3/settings/index.ts index e904eaedb8db0..9ac25b7fc9483 100644 --- a/x-pack/test/functional/apps/ml/settings/index.ts +++ b/x-pack/test/functional/apps/ml/group3/settings/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('settings', function () { diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/export_jobs.ts similarity index 99% rename from x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/export_jobs.ts index 85e249861378f..4ced89e35d608 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts +++ b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/export_jobs.ts @@ -7,7 +7,7 @@ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; import type { DataFrameAnalyticsConfig } from '@kbn/ml-plugin/public/application/data_frame_analytics/common'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const testADJobs: Array<{ job: Job; datafeed: Datafeed }> = [ { diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json similarity index 100% rename from x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/bad_data.json similarity index 100% rename from x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/bad_data.json diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json similarity index 100% rename from x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/import_jobs.ts similarity index 92% rename from x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/import_jobs.ts index ef367797ef7e9..212bb029b6e0b 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts +++ b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/import_jobs.ts @@ -5,17 +5,15 @@ * 2.0. */ -import path from 'path'; - import { JobType } from '@kbn/ml-plugin/common/types/saved_objects'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); const testDataListPositive = [ { - filePath: path.join(__dirname, 'files_to_import', 'anomaly_detection_jobs_7.16.json'), + filePath: require.resolve('./files_to_import/anomaly_detection_jobs_7.16.json'), expected: { jobType: 'anomaly-detector' as JobType, jobIds: ['ad-test1', 'ad-test3'], @@ -23,7 +21,7 @@ export default function ({ getService }: FtrProviderContext) { }, }, { - filePath: path.join(__dirname, 'files_to_import', 'data_frame_analytics_jobs_7.16.json'), + filePath: require.resolve('./files_to_import/data_frame_analytics_jobs_7.16.json'), expected: { jobType: 'data-frame-analytics' as JobType, jobIds: ['dfa-test1'], @@ -107,7 +105,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('selects job import'); await ml.stackManagementJobs.openImportFlyout(); await ml.stackManagementJobs.selectFileToImport( - path.join(__dirname, 'files_to_import', 'bad_data.json'), + require.resolve('./files_to_import/bad_data.json'), true ); }); diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/index.ts similarity index 89% rename from x-pack/test/functional/apps/ml/stack_management_jobs/index.ts rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/index.ts index c5e0728266bab..4c4bedfeb9b76 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts +++ b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('stack management jobs', function () { diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/manage_spaces.ts similarity index 99% rename from x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/manage_spaces.ts index 4841fca9d74f7..5563bb9043c7f 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts +++ b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/manage_spaces.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const browser = getService('browser'); diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/synchronize.ts b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/synchronize.ts similarity index 98% rename from x-pack/test/functional/apps/ml/stack_management_jobs/synchronize.ts rename to x-pack/test/functional/apps/ml/group3/stack_management_jobs/synchronize.ts index cf22f29bc277f..e760549b7a151 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/synchronize.ts +++ b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/synchronize.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/monitoring/config.ts b/x-pack/test/functional/apps/monitoring/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index b8f6f223092f6..cfa29d2f784e6 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -7,7 +7,6 @@ export default function ({ loadTestFile }) { describe('Monitoring app', function () { - this.tags('ciGroup28'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./cluster/list')); diff --git a/x-pack/test/functional/apps/remote_clusters/config.ts b/x-pack/test/functional/apps/remote_clusters/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/remote_clusters/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/remote_clusters/index.ts b/x-pack/test/functional/apps/remote_clusters/index.ts index a1cc660b6426a..74c4ce6e68bfc 100644 --- a/x-pack/test/functional/apps/remote_clusters/index.ts +++ b/x-pack/test/functional/apps/remote_clusters/index.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; // https://www.elastic.co/guide/en/kibana/7.9/working-remote-clusters.html export default ({ loadTestFile }: FtrProviderContext) => { describe('Remote Clusters app', function () { - this.tags(['ciGroup4', 'skipCloud']); + this.tags('skipCloud'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); diff --git a/x-pack/test/functional/apps/reporting/README.md b/x-pack/test/functional/apps/reporting/README.md index ec9bba8b88341..5d47804fef284 100644 --- a/x-pack/test/functional/apps/reporting/README.md +++ b/x-pack/test/functional/apps/reporting/README.md @@ -5,7 +5,7 @@ Functional tests on report generation are under the applications that use report **PDF/PNG Report testing:** - `x-pack/test/functional/apps/canvas/reports.ts` - `x-pack/test/functional/apps/dashboard/reporting/screenshots.ts` - - `x-pack/test/functional/apps/lens/lens_reporting.ts` + - `x-pack/test/functional/apps/lens/group3/lens_reporting.ts` - `x-pack/test/functional/apps/visualize/reporting.ts` **CSV Report testing:** diff --git a/x-pack/test/functional/apps/reporting_management/config.ts b/x-pack/test/functional/apps/reporting_management/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/reporting_management/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/reporting_management/index.js b/x-pack/test/functional/apps/reporting_management/index.js index 4e6abe85a4041..dcf5583eeb92a 100644 --- a/x-pack/test/functional/apps/reporting_management/index.js +++ b/x-pack/test/functional/apps/reporting_management/index.js @@ -7,7 +7,6 @@ export default ({ loadTestFile }) => { describe('reporting management app', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./report_listing')); }); }; diff --git a/x-pack/test/functional/apps/rollup_job/config.ts b/x-pack/test/functional/apps/rollup_job/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/rollup_job/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/rollup_job/index.js b/x-pack/test/functional/apps/rollup_job/index.js index 8fa9cd6f7aa72..943536539c5ad 100644 --- a/x-pack/test/functional/apps/rollup_job/index.js +++ b/x-pack/test/functional/apps/rollup_job/index.js @@ -7,8 +7,6 @@ export default function ({ loadTestFile }) { describe('rollup app', function () { - this.tags('ciGroup28'); - loadTestFile(require.resolve('./rollup_jobs')); loadTestFile(require.resolve('./hybrid_index_pattern')); loadTestFile(require.resolve('./tsvb')); diff --git a/x-pack/test/functional/apps/saved_objects_management/config.ts b/x-pack/test/functional/apps/saved_objects_management/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/saved_objects_management/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/saved_objects_management/exports/_7.13_import_saved_objects.ndjson b/x-pack/test/functional/apps/saved_objects_management/exports/_7.13_import_saved_objects.ndjson index 21be31349af0f..8f2aa535f305c 100644 --- a/x-pack/test/functional/apps/saved_objects_management/exports/_7.13_import_saved_objects.ndjson +++ b/x-pack/test/functional/apps/saved_objects_management/exports/_7.13_import_saved_objects.ndjson @@ -1,87 +1,83 @@ -{"attributes":{"accessCount":0,"accessDate":1621977234367,"createDate":1621977234367,"url":"/app/dashboards#/view/154944b0-6249-11eb-aebf-c306684b328d?embed=true&_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&_a=(description:%27%27,filters:!(),fullScreenMode:!f,options:(darkTheme:!f,hidePanelTitles:!f,useMargins:!t),panels:!((embeddableConfig:(enhancements:()),gridData:(h:15,i:%271%27,w:24,x:0,y:0),id:%2736b91810-6239-11eb-aebf-c306684b328d%27,panelIndex:%271%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:(),vis:!n),gridData:(h:15,i:%272%27,w:24,x:24,y:0),id:%270a274320-61cc-11eb-aebf-c306684b328d%27,panelIndex:%272%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%273%27,w:24,x:0,y:15),id:e4aef350-623d-11eb-aebf-c306684b328d,panelIndex:%273%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%274%27,w:24,x:24,y:15),id:f92e5630-623e-11eb-aebf-c306684b328d,panelIndex:%274%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:(),vis:!n),gridData:(h:15,i:%275%27,w:24,x:0,y:30),id:%279853d4d0-623d-11eb-aebf-c306684b328d%27,panelIndex:%275%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:(),vis:!n),gridData:(h:15,i:%276%27,w:24,x:24,y:30),id:%276ecb33b0-623d-11eb-aebf-c306684b328d%27,panelIndex:%276%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:(),vis:!n),gridData:(h:15,i:%277%27,w:24,x:0,y:45),id:b8e35c80-623c-11eb-aebf-c306684b328d,panelIndex:%277%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%278%27,w:24,x:24,y:45),id:f1bc75d0-6239-11eb-aebf-c306684b328d,panelIndex:%278%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%279%27,w:24,x:0,y:60),id:%270d8a8860-623a-11eb-aebf-c306684b328d%27,panelIndex:%279%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2710%27,w:24,x:24,y:60),id:d79fe3d0-6239-11eb-aebf-c306684b328d,panelIndex:%2710%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2711%27,w:24,x:0,y:75),id:%27318375a0-6240-11eb-aebf-c306684b328d%27,panelIndex:%2711%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2712%27,w:24,x:24,y:75),id:e461eb20-6245-11eb-aebf-c306684b328d,panelIndex:%2712%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2713%27,w:24,x:0,y:90),id:%2725bdc750-6242-11eb-aebf-c306684b328d%27,panelIndex:%2713%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2714%27,w:24,x:24,y:90),id:%2771dd7bc0-6248-11eb-aebf-c306684b328d%27,panelIndex:%2714%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2715%27,w:24,x:0,y:105),id:%276aea48a0-6240-11eb-aebf-c306684b328d%27,panelIndex:%2715%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2716%27,w:24,x:24,y:105),id:%2732b681f0-6241-11eb-aebf-c306684b328d%27,panelIndex:%2716%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2717%27,w:24,x:0,y:120),id:ccca99e0-6244-11eb-aebf-c306684b328d,panelIndex:%2717%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2718%27,w:24,x:24,y:120),id:a4d7be80-6245-11eb-aebf-c306684b328d,panelIndex:%2718%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2719%27,w:24,x:0,y:135),id:c94d8440-6248-11eb-aebf-c306684b328d,panelIndex:%2719%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2720%27,w:24,x:24,y:135),id:db6226f0-61c0-11eb-aebf-c306684b328d,panelIndex:%2720%27,type:search,version:%277.13.1%27)),query:(language:lucene,query:%27%27),tags:!(),timeRestore:!f,title:logstash_dashboardwithtime,viewMode:view)"},"coreMigrationVersion":"7.13.2","id":"058bc10f0578013fc41ddedc9a1dcd1e","references":[],"sort":[1623693556928,448],"type":"url","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NDUsNF0="} -{"attributes":{"color":"#ba898f","description":"","name":"By value tag"},"coreMigrationVersion":"7.13.2","id":"07f48f70-ca29-11eb-bf5e-3de94e83d4f0","references":[],"sort":[1623415891791,116],"type":"tag","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0MzEsNF0="} -{"attributes":{"fieldAttrs":"{\"ip\":{\"count\":2},\"geo.dest\":{\"count\":1}}","fields":"[]","runtimeFieldMap":"{}","timeFieldName":"@timestamp","title":"logstash-*"},"coreMigrationVersion":"7.13.2","id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"sort":[1623415891791,120],"type":"index-pattern","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0MzMsNF0="} -{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"60aaea59-d871-4e90-9ff3-78946d6bef90\"},\"panelIndex\":\"60aaea59-d871-4e90-9ff3-78946d6bef90\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsPie\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"2174c5fe-75d2-43ae-9c79-1a7cc7bbbaea\":{\"columns\":{\"65625f0d-e7f1-4370-b939-7db27af74de7\":{\"label\":\"Top values of geo.srcdest\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"geo.srcdest\",\"isBucketed\":true,\"params\":{\"size\":18,\"orderBy\":{\"type\":\"column\",\"columnId\":\"553a353f-dac5-4a52-a25d-52c6e1462597\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"553a353f-dac5-4a52-a25d-52c6e1462597\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"}},\"columnOrder\":[\"65625f0d-e7f1-4370-b939-7db27af74de7\",\"553a353f-dac5-4a52-a25d-52c6e1462597\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"shape\":\"donut\",\"layers\":[{\"layerId\":\"2174c5fe-75d2-43ae-9c79-1a7cc7bbbaea\",\"groups\":[\"65625f0d-e7f1-4370-b939-7db27af74de7\",\"65625f0d-e7f1-4370-b939-7db27af74de7\",\"65625f0d-e7f1-4370-b939-7db27af74de7\"],\"metric\":\"553a353f-dac5-4a52-a25d-52c6e1462597\",\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"default\",\"nestedLegend\":false}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"43fcac20-ca27-11eb-bf5e-3de94e83d4f0\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"43fcac20-ca27-11eb-bf5e-3de94e83d4f0\",\"name\":\"indexpattern-datasource-layer-2174c5fe-75d2-43ae-9c79-1a7cc7bbbaea\"}]},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"9ed45b8b-534b-4fac-9fee-436896b90039\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"circular drilldown\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}},\"type\":\"lens\"}}]","timeRestore":false,"title":"lens_panel_drilldown","version":1},"coreMigrationVersion":"7.13.2","id":"35ce3b30-ca29-11eb-bf5e-3de94e83d4f0","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"indexpattern-datasource-layer-2174c5fe-75d2-43ae-9c79-1a7cc7bbbaea","type":"index-pattern"},{"id":"08dec860-ca29-11eb-bf5e-3de94e83d4f0","name":"drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:9ed45b8b-534b-4fac-9fee-436896b90039:dashboardId","type":"dashboard"},{"id":"07f48f70-ca29-11eb-bf5e-3de94e83d4f0","name":"tag-07f48f70-ca29-11eb-bf5e-3de94e83d4f0","type":"tag"}],"sort":[1623415891791,125],"type":"dashboard","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0MzQsNF0="} -{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"095e13b2-d0ac-47db-a62b-0aca28931402\"},\"panelIndex\":\"095e13b2-d0ac-47db-a62b-0aca28931402\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"f61694eb-94ed-495d-9ce8-63592f040b0b\":{\"columns\":{\"75ddcdb4-3050-4545-b401-509384b0d532\":{\"label\":\"Top values of machine.os.raw\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"machine.os.raw\",\"isBucketed\":true,\"params\":{\"size\":3,\"orderBy\":{\"type\":\"column\",\"columnId\":\"2f97b0f5-f0ff-40f2-abb0-bb7d1081a126\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"273e31ef-7c2d-4d0e-9063-5528f4011a51\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\"}},\"2f97b0f5-f0ff-40f2-abb0-bb7d1081a126\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"},\"2eb30654-0ead-40ac-92ab-d8d113e25ac5\":{\"label\":\"Average of bytes\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"bytes\",\"isBucketed\":false,\"scale\":\"ratio\"}},\"columnOrder\":[\"75ddcdb4-3050-4545-b401-509384b0d532\",\"273e31ef-7c2d-4d0e-9063-5528f4011a51\",\"2f97b0f5-f0ff-40f2-abb0-bb7d1081a126\",\"2eb30654-0ead-40ac-92ab-d8d113e25ac5\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"f61694eb-94ed-495d-9ce8-63592f040b0b\",\"accessors\":[\"2f97b0f5-f0ff-40f2-abb0-bb7d1081a126\",\"2eb30654-0ead-40ac-92ab-d8d113e25ac5\"],\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"xAccessor\":\"273e31ef-7c2d-4d0e-9063-5528f4011a51\",\"splitAccessor\":\"75ddcdb4-3050-4545-b401-509384b0d532\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"43fcac20-ca27-11eb-bf5e-3de94e83d4f0\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"43fcac20-ca27-11eb-bf5e-3de94e83d4f0\",\"name\":\"indexpattern-datasource-layer-f61694eb-94ed-495d-9ce8-63592f040b0b\"}]},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"da7ad2b6-1e4a-40b5-9123-d0ec2bde858d\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"lens_panel_drilldown\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}},\"type\":\"lens\"}},{\"version\":\"7.13.1\",\"type\":\"map\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"153bb2dc-c4f5-4fcc-a45a-4e61cdaad8c2\"},\"panelIndex\":\"153bb2dc-c4f5-4fcc-a45a-4e61cdaad8c2\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"layerListJSON\":\"[{\\\"sourceDescriptor\\\":{\\\"type\\\":\\\"EMS_TMS\\\",\\\"isAutoSelect\\\":true},\\\"id\\\":\\\"16b378f6-4b68-4d17-8be4-3da333440869\\\",\\\"label\\\":null,\\\"minZoom\\\":0,\\\"maxZoom\\\":24,\\\"alpha\\\":1,\\\"visible\\\":true,\\\"style\\\":{\\\"type\\\":\\\"TILE\\\"},\\\"type\\\":\\\"VECTOR_TILE\\\"},{\\\"sourceDescriptor\\\":{\\\"indexPatternId\\\":\\\"43fcac20-ca27-11eb-bf5e-3de94e83d4f0\\\",\\\"geoField\\\":\\\"geo.coordinates\\\",\\\"filterByMapBounds\\\":true,\\\"scalingType\\\":\\\"CLUSTERS\\\",\\\"topHitsSplitField\\\":\\\"\\\",\\\"topHitsSize\\\":1,\\\"id\\\":\\\"86ff9ac9-9ccf-44b0-8b90-a4b779f5bc38\\\",\\\"type\\\":\\\"ES_SEARCH\\\",\\\"applyGlobalQuery\\\":true,\\\"applyGlobalTime\\\":true,\\\"tooltipProperties\\\":[\\\"extension\\\",\\\"geo.dest\\\",\\\"response\\\"],\\\"sortField\\\":\\\"\\\",\\\"sortOrder\\\":\\\"desc\\\"},\\\"id\\\":\\\"3d062d54-73de-4be7-8ae2-11e9b385f7d9\\\",\\\"label\\\":\\\"\\\",\\\"minZoom\\\":0,\\\"maxZoom\\\":24,\\\"alpha\\\":0.75,\\\"visible\\\":true,\\\"style\\\":{\\\"type\\\":\\\"VECTOR\\\",\\\"properties\\\":{\\\"icon\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"value\\\":\\\"marker\\\"}},\\\"fillColor\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"color\\\":\\\"#54B399\\\"}},\\\"lineColor\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"color\\\":\\\"#41937c\\\"}},\\\"lineWidth\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"size\\\":1}},\\\"iconSize\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"size\\\":6}},\\\"iconOrientation\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"orientation\\\":0}},\\\"labelText\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"value\\\":\\\"\\\"}},\\\"labelColor\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"color\\\":\\\"#000000\\\"}},\\\"labelSize\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"size\\\":14}},\\\"labelBorderColor\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"color\\\":\\\"#FFFFFF\\\"}},\\\"symbolizeAs\\\":{\\\"options\\\":{\\\"value\\\":\\\"circle\\\"}},\\\"labelBorderSize\\\":{\\\"options\\\":{\\\"size\\\":\\\"SMALL\\\"}}},\\\"isTimeAware\\\":true},\\\"type\\\":\\\"BLENDED_VECTOR\\\",\\\"joins\\\":[]}]\",\"mapStateJSON\":\"{\\\"zoom\\\":1.38,\\\"center\\\":{\\\"lon\\\":0,\\\"lat\\\":19.94277},\\\"timeFilters\\\":{\\\"from\\\":\\\"now-15y\\\",\\\"to\\\":\\\"now\\\"},\\\"refreshConfig\\\":{\\\"isPaused\\\":true,\\\"interval\\\":0},\\\"query\\\":{\\\"query\\\":\\\"\\\",\\\"language\\\":\\\"kuery\\\"},\\\"filters\\\":[],\\\"settings\\\":{\\\"autoFitToDataBounds\\\":false,\\\"backgroundColor\\\":\\\"#ffffff\\\",\\\"disableInteractive\\\":false,\\\"disableTooltipControl\\\":false,\\\"hideToolbarOverlay\\\":false,\\\"hideLayerControl\\\":false,\\\"hideViewControl\\\":false,\\\"initialLocation\\\":\\\"LAST_SAVED_LOCATION\\\",\\\"fixedLocation\\\":{\\\"lat\\\":0,\\\"lon\\\":0,\\\"zoom\\\":2},\\\"browserLocation\\\":{\\\"zoom\\\":2},\\\"maxZoom\\\":24,\\\"minZoom\\\":0,\\\"showScaleControl\\\":false,\\\"showSpatialFilters\\\":true,\\\"spatialFiltersAlpa\\\":0.3,\\\"spatialFiltersFillColor\\\":\\\"#DA8B45\\\",\\\"spatialFiltersLineColor\\\":\\\"#DA8B45\\\"}}\",\"uiStateJSON\":\"{\\\"isLayerTOCOpen\\\":true,\\\"openTOCDetails\\\":[]}\"},\"mapCenter\":{\"lat\":19.94277,\"lon\":0,\"zoom\":1.38},\"mapBuffer\":{\"minLon\":-214.7723,\"minLat\":-74.644155,\"maxLon\":214.7723,\"maxLat\":102.864625},\"isLayerTOCOpen\":true,\"openTOCDetails\":[],\"hiddenLayers\":[],\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"b10e564b-7d42-45f5-8c26-a9220c405834\",\"triggers\":[\"CONTEXT_MENU_TRIGGER\"],\"action\":{\"name\":\"URL drilldown\",\"config\":{\"url\":{\"template\":\"http://localhost:5601/app/discover#/view/0abce1c0-ca2a-11eb-bf5e-3de94e83d4f0?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&_a=(columns:!(),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'43fcac20-ca27-11eb-bf5e-3de94e83d4f0',key:geo.srcdest,negate:!f,params:!('IN:CN'),type:phrases,value:'IN:CN'),query:(bool:(minimum_should_match:1,should:!((match_phrase:(geo.srcdest:'IN:CN'))))))),index:'43fcac20-ca27-11eb-bf5e-3de94e83d4f0',interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))\"},\"openInNewTab\":true,\"encodeUrl\":true},\"factoryId\":\"URL_DRILLDOWN\"}}]}},\"type\":\"map\"}},{\"version\":\"7.13.1\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"5de83d82-bbd1-4d30-be61-dd6724f32c07\"},\"panelIndex\":\"5de83d82-bbd1-4d30-be61-dd6724f32c07\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"metrics\",\"params\":{\"annotations\":[{\"fields\":\"response.raw\",\"template\":\"{{response.raw}}\",\"index_pattern\":\"logstash-*\",\"query_string\":{\"query\":\"response.raw :\\\"404\\\" \",\"language\":\"kuery\"},\"color\":\"#F00\",\"icon\":\"fa-bomb\",\"id\":\"37395960-ca28-11eb-9eac-2f3ccefcbeef\",\"ignore_global_filters\":1,\"ignore_panel_filters\":1,\"time_field\":\"@timestamp\"}],\"axis_formatter\":\"number\",\"axis_position\":\"left\",\"axis_scale\":\"normal\",\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"index_pattern\":\"logstash-*\",\"interval\":\"\",\"isModelInvalid\":false,\"series\":[{\"axis_position\":\"right\",\"chart_type\":\"line\",\"color\":\"#68BC00\",\"fill\":0.5,\"formatter\":\"number\",\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"line_width\":1,\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"count\"}],\"point_size\":1,\"separate_axis\":0,\"split_color_mode\":\"kibana\",\"split_mode\":\"everything\",\"stacked\":\"none\"}],\"show_grid\":1,\"show_legend\":1,\"time_field\":\"@timestamp\",\"tooltip_mode\":\"show_all\",\"type\":\"timeseries\",\"use_kibana_indexes\":false},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"23d04651-266f-4f0a-8eef-6f190f0a84af\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"dashboard\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}},\"type\":\"visualization\"}},{\"version\":\"7.13.1\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"a254a623-a9af-4372-851b-572fa95b0902\"},\"panelIndex\":\"a254a623-a9af-4372-851b-572fa95b0902\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"vega\",\"params\":{\"spec\":\"{\\n/*\\n\\nWelcome to Vega visualizations. Here you can design your own dataviz from scratch using a declarative language called Vega, or its simpler form Vega-Lite. In Vega, you have the full control of what data is loaded, even from multiple sources, how that data is transformed, and what visual elements are used to show it. Use help icon to view Vega examples, tutorials, and other docs. Use the wrench icon to reformat this text, or to remove comments.\\n\\nThis example graph shows the document count in all indexes in the current time range. You might need to adjust the time filter in the upper right corner.\\n*/\\n\\n $schema: https://vega.github.io/schema/vega-lite/v4.json\\n title: Event counts from all indexes\\n\\n // Define the data source\\n data: {\\n url: {\\n/*\\nAn object instead of a string for the \\\"url\\\" param is treated as an Elasticsearch query. Anything inside this object is not part of the Vega language, but only understood by Kibana and Elasticsearch server. This query counts the number of documents per time interval, assuming you have a @timestamp field in your data.\\n\\nKibana has a special handling for the fields surrounded by \\\"%\\\". They are processed before the the query is sent to Elasticsearch. This way the query becomes context aware, and can use the time range and the dashboard filters.\\n*/\\n\\n // Apply dashboard context filters when set\\n %context%: true\\n // Filter the time picker (upper right corner) with this field\\n %timefield%: @timestamp\\n\\n/*\\nSee .search() documentation for : https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/api-reference.html#api-search\\n*/\\n\\n // Which index to search\\n index: _all\\n // Aggregate data by the time field into time buckets, counting the number of documents in each bucket.\\n body: {\\n aggs: {\\n time_buckets: {\\n date_histogram: {\\n // Use date histogram aggregation on @timestamp field\\n field: @timestamp\\n // The interval value will depend on the daterange picker (true), or use an integer to set an approximate bucket count\\n interval: {%autointerval%: true}\\n // Make sure we get an entire range, even if it has no data\\n extended_bounds: {\\n // Use the current time range's start and end\\n min: {%timefilter%: \\\"min\\\"}\\n max: {%timefilter%: \\\"max\\\"}\\n }\\n // Use this for linear (e.g. line, area) graphs. Without it, empty buckets will not show up\\n min_doc_count: 0\\n }\\n }\\n }\\n // Speed up the response by only including aggregation results\\n size: 0\\n }\\n }\\n/*\\nElasticsearch will return results in this format:\\n\\naggregations: {\\n time_buckets: {\\n buckets: [\\n {\\n key_as_string: 2015-11-30T22:00:00.000Z\\n key: 1448920800000\\n doc_count: 0\\n },\\n {\\n key_as_string: 2015-11-30T23:00:00.000Z\\n key: 1448924400000\\n doc_count: 0\\n }\\n ...\\n ]\\n }\\n}\\n\\nFor our graph, we only need the list of bucket values. Use the format.property to discard everything else.\\n*/\\n format: {property: \\\"aggregations.time_buckets.buckets\\\"}\\n }\\n\\n // \\\"mark\\\" is the graphics element used to show our data. Other mark values are: area, bar, circle, line, point, rect, rule, square, text, and tick. See https://vega.github.io/vega-lite/docs/mark.html\\n mark: line\\n\\n // \\\"encoding\\\" tells the \\\"mark\\\" what data to use and in what way. See https://vega.github.io/vega-lite/docs/encoding.html\\n encoding: {\\n x: {\\n // The \\\"key\\\" value is the timestamp in milliseconds. Use it for X axis.\\n field: key\\n type: temporal\\n axis: {title: false} // Customize X axis format\\n }\\n y: {\\n // The \\\"doc_count\\\" is the count per bucket. Use it for Y axis.\\n field: doc_count\\n type: quantitative\\n axis: {title: \\\"Document count\\\"}\\n }\\n }\\n}\\n\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[{\"$state\":{\"store\":\"appState\"},\"meta\":{\"alias\":null,\"disabled\":false,\"index\":\"43fcac20-ca27-11eb-bf5e-3de94e83d4f0\",\"key\":\"geo.srcdest\",\"negate\":false,\"params\":{\"query\":\"CN:CN\"},\"type\":\"phrase\"},\"query\":{\"match_phrase\":{\"geo.srcdest\":\"CN:CN\"}}}]}}},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"cfd2521d-15a0-4c64-b0ab-d2dc18f396e3\",\"triggers\":[\"CONTEXT_MENU_TRIGGER\"],\"action\":{\"name\":\"URL\",\"config\":{\"url\":{\"template\":\"http://localhost:5601/app/discover#/view/4acce030-ca2a-11eb-bf5e-3de94e83d4f0?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&_a=(columns:!(),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'43fcac20-ca27-11eb-bf5e-3de94e83d4f0',key:ip,negate:!f,params:(query:'57.237.11.219'),type:phrase),query:(match_phrase:(ip:'57.237.11.219')))),index:'43fcac20-ca27-11eb-bf5e-3de94e83d4f0',interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))\"},\"openInNewTab\":true,\"encodeUrl\":true},\"factoryId\":\"URL_DRILLDOWN\"}}]}},\"type\":\"visualization\"}},{\"version\":\"7.13.1\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"7f0506ed-1f30-410f-bcd7-3f70623aa5ba\"},\"panelIndex\":\"7f0506ed-1f30-410f-bcd7-3f70623aa5ba\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"significant_terms\",\"params\":{\"field\":\"geo.srcdest\",\"size\":77},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"43fcac20-ca27-11eb-bf5e-3de94e83d4f0\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"enhancements\":{},\"type\":\"visualization\"}}]","timeRestore":false,"title":"logstash_by_value_dashboard","version":1},"coreMigrationVersion":"7.13.2","id":"08dec860-ca29-11eb-bf5e-3de94e83d4f0","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"indexpattern-datasource-layer-f61694eb-94ed-495d-9ce8-63592f040b0b","type":"index-pattern"},{"id":"35ce3b30-ca29-11eb-bf5e-3de94e83d4f0","name":"drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:da7ad2b6-1e4a-40b5-9123-d0ec2bde858d:dashboardId","type":"dashboard"},{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"layer_1_source_index_pattern","type":"index-pattern"},{"id":"35ce3b30-ca29-11eb-bf5e-3de94e83d4f0","name":"drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:23d04651-266f-4f0a-8eef-6f190f0a84af:dashboardId","type":"dashboard"},{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"},{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"07f48f70-ca29-11eb-bf5e-3de94e83d4f0","name":"tag-07f48f70-ca29-11eb-bf5e-3de94e83d4f0","type":"tag"}],"sort":[1623415891791,134],"type":"dashboard","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0MzUsNF0="} -{"attributes":{"fieldAttrs":"{\"machine.os\":{\"count\":1},\"spaces\":{\"count\":1},\"type\":{\"count\":1},\"bytes_scripted\":{\"count\":1}}","fields":"[{\"count\":1,\"script\":\"doc['bytes'].value*1024\",\"lang\":\"painless\",\"name\":\"bytes_scripted\",\"type\":\"number\",\"scripted\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false}]","runtimeFieldMap":"{}","timeFieldName":"@timestamp","title":"logstash-*"},"coreMigrationVersion":"7.13.2","id":"56b34100-619d-11eb-aebf-c306684b328d","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"sort":[1623693556928,449],"type":"index-pattern","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NDYsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_scriptedfieldviz","uiStateJSON":"{\"vis\":{\"defaultColors\":{\"0 - 100\":\"rgb(0,104,55)\"}}}","version":1,"visState":"{\"title\":\"logstash_scriptedfieldviz\",\"type\":\"goal\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"isDisplayWarning\":false,\"type\":\"gauge\",\"gauge\":{\"verticalSplit\":false,\"autoExtend\":false,\"percentageMode\":true,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\",\"width\":2},\"type\":\"meter\",\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"range\",\"schema\":\"group\",\"params\":{\"field\":\"bytes_scripted\",\"ranges\":[{\"from\":0,\"to\":40000},{\"from\":40001,\"to\":20000000}]}}]}"},"coreMigrationVersion":"7.13.2","id":"0a274320-61cc-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,451],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NDcsNF0="} -{"attributes":{"columns":[],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[{\"meta\":{\"type\":\"phrases\",\"key\":\"geo.srcdest\",\"value\":\"IN:CN\",\"params\":[\"IN:CN\"],\"alias\":null,\"negate\":false,\"disabled\":false,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"bool\":{\"should\":[{\"match_phrase\":{\"geo.srcdest\":\"IN:CN\"}}],\"minimum_should_match\":1}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["@timestamp","desc"]],"title":"search_saved","version":1},"coreMigrationVersion":"7.13.2","id":"0abce1c0-ca2a-11eb-bf5e-3de94e83d4f0","migrationVersion":{"search":"7.9.3"},"references":[{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"}],"sort":[1623415891791,137],"type":"search","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0MzYsNF0="} -{"attributes":{"color":"#81a93f","description":"","name":"logstash_tag"},"coreMigrationVersion":"7.13.2","id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","references":[],"sort":[1623693556928,452],"type":"tag","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NDgsNF0="} -{"attributes":{"description":"","layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"4c2394ca-a6a2-4f8d-9631-259eb3a9627f\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"},{\"sourceDescriptor\":{\"geoField\":\"geo.coordinates\",\"filterByMapBounds\":true,\"scalingType\":\"CLUSTERS\",\"id\":\"7555324e-e793-4b7d-a9d2-cd63e6b7fe3d\",\"type\":\"ES_SEARCH\",\"applyGlobalQuery\":true,\"applyGlobalTime\":true,\"tooltipProperties\":[\"geo.srcdest\",\"machine.os\",\"type\"],\"sortField\":\"bytes_scripted\",\"sortOrder\":\"desc\",\"topHitsSplitField\":\"\",\"topHitsSize\":1,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"id\":\"6a493d8b-a220-46bc-8906-a1a7569799e0\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"extension.raw\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":3},\"type\":\"CATEGORICAL\"}},\"lineColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"machine.os.raw\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":3},\"type\":\"CATEGORICAL\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true},\"type\":\"BLENDED_VECTOR\",\"joins\":[]}]","mapStateJSON":"{\"zoom\":1.56,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-15y\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"backgroundColor\":\"#ffffff\",\"disableInteractive\":false,\"disableTooltipControl\":false,\"hideToolbarOverlay\":false,\"hideLayerControl\":false,\"hideViewControl\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showScaleControl\":false,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}","title":"logstash_maps","uiStateJSON":"{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}"},"coreMigrationVersion":"7.13.2","id":"0c5974f0-be5c-11eb-9520-1b4c3ca6a781","migrationVersion":{"map":"7.12.0"},"references":[{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"layer_1_source_index_pattern","type":"index-pattern"}],"sort":[1623693556928,455],"type":"map","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NDksNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_datatable","uiStateJSON":"{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}","version":1,"visState":"{\"title\":\"logstash_datatable\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":true,\"totalFunc\":\"sum\",\"showToolbar\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"bucket\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-07-24T08:58:14.175Z\",\"to\":\"2015-11-11T13:28:17.223Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"response.raw\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.2","id":"0d8a8860-623a-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,457],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NTAsNF0="} -{"attributes":{"description":null,"state":{"datasourceStates":{"indexpattern":{"layers":{"35fd070e-5bbc-4906-bf69-8548a213d7a0":{"columnOrder":["2bf7969f-0371-4df2-a398-0a191e428ce5","aab812d6-609b-444d-9990-1e67f85fd85d","e9829e8a-c484-4c9d-b489-f1eb3fb138d2","4fc9fb3b-29a5-4679-ab3c-90d5daaf0661"],"columns":{"2bf7969f-0371-4df2-a398-0a191e428ce5":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"4fc9fb3b-29a5-4679-ab3c-90d5daaf0661":{"dataType":"number","isBucketed":false,"label":"Moving average of Median of bytes","operationType":"moving_average","params":{"window":5},"references":["e9829e8a-c484-4c9d-b489-f1eb3fb138d2"],"scale":"ratio"},"aab812d6-609b-444d-9990-1e67f85fd85d":{"dataType":"number","isBucketed":false,"label":"Average of bytes","operationType":"average","scale":"ratio","sourceField":"bytes"},"e9829e8a-c484-4c9d-b489-f1eb3fb138d2":{"dataType":"number","isBucketed":false,"label":"Median of bytes","operationType":"median","scale":"ratio","sourceField":"bytes"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["aab812d6-609b-444d-9990-1e67f85fd85d","4fc9fb3b-29a5-4679-ab3c-90d5daaf0661"],"layerId":"35fd070e-5bbc-4906-bf69-8548a213d7a0","position":"top","seriesType":"bar_stacked","showGridlines":false,"xAccessor":"2bf7969f-0371-4df2-a398-0a191e428ce5"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_barvertical_stacked_average","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.2","id":"0dbbf8b0-be3c-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-35fd070e-5bbc-4906-bf69-8548a213d7a0","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,461],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NTEsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_area_chart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_area_chart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2010-01-28T19:25:55.242Z\",\"to\":\"2021-01-28T19:40:55.242Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"machine.os.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"machine OS\"}}]}"},"coreMigrationVersion":"7.13.2","id":"36b91810-6239-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,463],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NTIsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_horizontal","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_horizontal\",\"type\":\"horizontal_bar\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":200},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":75,\"filter\":true,\"truncate\":100},\"title\":{\"text\":\"no of documents\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"normal\",\"data\":{\"label\":\"no of documents\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":true,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{\"customLabel\":\"no of documents\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-07-24T08:58:14.175Z\",\"to\":\"2015-11-11T13:28:17.223Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"extension.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.2","id":"e4aef350-623d-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,465],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NTMsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_linechart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_linechart\",\"type\":\"line\",\"params\":{\"type\":\"line\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"line\",\"mode\":\"normal\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"radiusRatio\":51,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-09-18T06:38:43.311Z\",\"to\":\"2015-09-26T04:02:51.104Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"sum\",\"schema\":\"radius\",\"params\":{\"field\":\"bytes\",\"customLabel\":\"bubbles\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"machine.os.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.2","id":"f92e5630-623e-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,467],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NTQsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_heatmap","uiStateJSON":"{\"vis\":{\"defaultColors\":{\"0% - 25%\":\"rgb(255,255,204)\",\"25% - 50%\":\"rgb(254,217,118)\",\"50% - 75%\":\"rgb(253,141,60)\",\"75% - 100%\":\"rgb(227,27,28)\"}}}","version":1,"visState":"{\"title\":\"logstash_heatmap\",\"type\":\"heatmap\",\"params\":{\"type\":\"heatmap\",\"addTooltip\":true,\"addLegend\":true,\"enableHover\":false,\"legendPosition\":\"right\",\"times\":[],\"colorsNumber\":4,\"colorSchema\":\"Yellow to Red\",\"setColorRange\":false,\"colorsRange\":[],\"invertColors\":false,\"percentageMode\":true,\"valueAxes\":[{\"show\":false,\"id\":\"ValueAxis-1\",\"type\":\"value\",\"scale\":{\"type\":\"linear\",\"defaultYExtents\":false},\"labels\":{\"show\":false,\"rotate\":0,\"overwriteColor\":false,\"color\":\"#555\"}}],\"row\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-07-24T08:58:14.175Z\",\"to\":\"2015-11-11T13:28:17.223Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"machine.os.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"split\",\"params\":{\"field\":\"response.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.2","id":"9853d4d0-623d-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,469],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NTUsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_goalchart","uiStateJSON":"{\"vis\":{\"defaultColors\":{\"0 - 33\":\"rgb(0,104,55)\",\"33 - 67\":\"rgb(255,255,190)\",\"67 - 100\":\"rgb(165,0,38)\"}}}","version":1,"visState":"{\"title\":\"logstash_goalchart\",\"type\":\"goal\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"isDisplayWarning\":false,\"type\":\"gauge\",\"gauge\":{\"verticalSplit\":false,\"autoExtend\":false,\"percentageMode\":true,\"gaugeType\":\"Circle\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000},{\"from\":10001,\"to\":20000},{\"from\":20001,\"to\":30000}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\",\"width\":2},\"type\":\"meter\",\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60},\"minAngle\":0,\"maxAngle\":6.283185307179586}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"group\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-07-24T08:58:14.175Z\",\"to\":\"2015-11-11T13:28:17.223Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}"},"coreMigrationVersion":"7.13.2","id":"6ecb33b0-623d-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,471],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NTYsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_gauge","uiStateJSON":"{\"vis\":{\"defaultColors\":{\"0 - 50\":\"rgb(0,104,55)\",\"50 - 75\":\"rgb(255,255,190)\",\"75 - 100\":\"rgb(165,0,38)\"}}}","version":1,"visState":"{\"title\":\"logstash_gauge\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":50},{\"from\":50,\"to\":75},{\"from\":75,\"to\":100}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"\",\"fontSize\":60,\"labelColor\":true},\"alignment\":\"horizontal\"}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"range\",\"schema\":\"group\",\"params\":{\"field\":\"bytes\",\"ranges\":[{\"from\":0,\"to\":10001},{\"from\":10002,\"to\":1000000}],\"json\":\"\"}}]}"},"coreMigrationVersion":"7.13.2","id":"b8e35c80-623c-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,473],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NTcsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_coordinatemaps","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_coordinatemaps\",\"type\":\"tile_map\",\"params\":{\"colorSchema\":\"Yellow to Red\",\"mapType\":\"Scaled Circle Markers\",\"isDesaturated\":false,\"addTooltip\":true,\"heatClusterSize\":1.5,\"legendPosition\":\"bottomright\",\"mapZoom\":2,\"mapCenter\":[0,0],\"wms\":{\"enabled\":false,\"options\":{\"format\":\"image/png\",\"transparent\":true},\"selectedTmsLayer\":{\"origin\":\"elastic_maps_service\",\"id\":\"road_map\",\"minZoom\":0,\"maxZoom\":18,\"attribution\":\"

© OpenStreetMap contributors|OpenMapTiles|Elastic Maps Service

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

© OpenStreetMap contributors|OpenMapTiles|Elastic Maps Service

\"}},\"selectedLayer\":{\"name\":\"World Countries\",\"origin\":\"elastic_maps_service\",\"id\":\"world_countries\",\"created_at\":\"2017-04-26T17:12:15.978370\",\"attribution\":\"Made with NaturalEarth | Elastic Maps Service\",\"fields\":[{\"type\":\"id\",\"name\":\"iso2\",\"description\":\"ISO 3166-1 alpha-2 code\"},{\"type\":\"id\",\"name\":\"iso3\",\"description\":\"ISO 3166-1 alpha-3 code\"},{\"type\":\"property\",\"name\":\"name\",\"description\":\"name\"}],\"format\":{\"type\":\"geojson\"},\"layerId\":\"elastic_maps_service.World Countries\",\"isEMS\":true}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.dest\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.2","id":"25bdc750-6242-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,482],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NjIsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_verticalbarchart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_verticalbarchart\",\"type\":\"histogram\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\",\"defaultYExtents\":true},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":true,\"row\":true,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-09-18T06:38:43.311Z\",\"to\":\"2015-09-26T04:02:51.104Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"h\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{},\"scaleMetricValues\":true}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"split\",\"params\":{\"field\":\"response.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Response code\"}}]}"},"coreMigrationVersion":"7.13.2","id":"71dd7bc0-6248-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,484],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NjMsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_metricviz","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_metricviz\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"range\",\"schema\":\"group\",\"params\":{\"field\":\"bytes_scripted\",\"ranges\":[{\"from\":0,\"to\":10000},{\"from\":10001,\"to\":300000}]}}]}"},"coreMigrationVersion":"7.13.2","id":"6aea48a0-6240-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,486],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NjQsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_piechart","uiStateJSON":"{}","version":1,"visState":"{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"field\":\"machine.os.raw\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"size\":5},\"schema\":\"segment\",\"type\":\"terms\"}],\"params\":{\"addLegend\":true,\"addTooltip\":true,\"isDonut\":true,\"labels\":{\"last_level\":true,\"show\":false,\"truncate\":100,\"values\":true},\"legendPosition\":\"right\",\"type\":\"pie\"},\"title\":\"logstash_piechart\",\"type\":\"pie\"}"},"coreMigrationVersion":"7.13.2","id":"32b681f0-6241-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,488],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NjUsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_tagcloud","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_tagcloud\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"log\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.srcdest\",\"size\":23,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.2","id":"ccca99e0-6244-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,490],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NjYsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"title":"logstash_timelion","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_timelion\",\"type\":\"timelion\",\"params\":{\"expression\":\".es(q='machine.os.raw:win xp' , index=logstash-*)\",\"interval\":\"auto\"},\"aggs\":[]}"},"coreMigrationVersion":"7.13.2","id":"a4d7be80-6245-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[],"sort":[1623693556928,491],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NjcsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{}"},"title":"logstash_tsvb","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_tsvb\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"count\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"axis_scale\":\"normal\",\"show_legend\":1,\"show_grid\":1,\"annotations\":[{\"fields\":\"machine.os.raw\",\"template\":\"{{machine.os.raw}}\",\"index_pattern\":\"logstash-*\",\"query_string\":{\"query\":\"machine.os.raw :\\\"win xp\\\" \",\"language\":\"lucene\"},\"id\":\"aa43ceb0-6248-11eb-9a82-ef1c6e6c0265\",\"color\":\"#F00\",\"time_field\":\"@timestamp\",\"icon\":\"fa-tag\",\"ignore_global_filters\":1,\"ignore_panel_filters\":1}],\"use_kibana_indexes\":false},\"aggs\":[]}"},"coreMigrationVersion":"7.13.2","id":"c94d8440-6248-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[],"sort":[1623693556928,492],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NjgsNF0="} -{"attributes":{"columns":["bytes_scripted"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"machine.os.raw :\\\"win xp\\\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["@timestamp","desc"]],"title":"logstash_scripted_saved_search","version":1},"coreMigrationVersion":"7.13.2","id":"db6226f0-61c0-11eb-aebf-c306684b328d","migrationVersion":{"search":"7.9.3"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,494],"type":"search","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NjksNF0="} -{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"3\",\"w\":24,\"x\":0,\"y\":15},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"4\",\"w\":24,\"x\":24,\"y\":15},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"5\",\"w\":24,\"x\":0,\"y\":30},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"6\",\"w\":24,\"x\":24,\"y\":30},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"7\",\"w\":24,\"x\":0,\"y\":45},\"panelIndex\":\"7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"8\",\"w\":24,\"x\":24,\"y\":45},\"panelIndex\":\"8\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"9\",\"w\":24,\"x\":0,\"y\":60},\"panelIndex\":\"9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"10\",\"w\":24,\"x\":24,\"y\":60},\"panelIndex\":\"10\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_10\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"11\",\"w\":24,\"x\":0,\"y\":75},\"panelIndex\":\"11\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_11\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"12\",\"w\":24,\"x\":24,\"y\":75},\"panelIndex\":\"12\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_12\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"13\",\"w\":24,\"x\":0,\"y\":90},\"panelIndex\":\"13\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"14\",\"w\":24,\"x\":24,\"y\":90},\"panelIndex\":\"14\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_14\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"15\",\"w\":24,\"x\":0,\"y\":105},\"panelIndex\":\"15\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_15\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"16\",\"w\":24,\"x\":24,\"y\":105},\"panelIndex\":\"16\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_16\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"17\",\"w\":24,\"x\":0,\"y\":120},\"panelIndex\":\"17\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_17\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"18\",\"w\":24,\"x\":24,\"y\":120},\"panelIndex\":\"18\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_18\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"19\",\"w\":24,\"x\":0,\"y\":135},\"panelIndex\":\"19\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_19\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"h\":15,\"i\":\"20\",\"w\":24,\"x\":24,\"y\":135},\"panelIndex\":\"20\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_20\"}]","timeRestore":false,"title":"logstash_dashboardwithtime","version":1},"coreMigrationVersion":"7.13.2","id":"154944b0-6249-11eb-aebf-c306684b328d","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"36b91810-6239-11eb-aebf-c306684b328d","name":"1:panel_1","type":"visualization"},{"id":"0a274320-61cc-11eb-aebf-c306684b328d","name":"2:panel_2","type":"visualization"},{"id":"e4aef350-623d-11eb-aebf-c306684b328d","name":"3:panel_3","type":"visualization"},{"id":"f92e5630-623e-11eb-aebf-c306684b328d","name":"4:panel_4","type":"visualization"},{"id":"9853d4d0-623d-11eb-aebf-c306684b328d","name":"5:panel_5","type":"visualization"},{"id":"6ecb33b0-623d-11eb-aebf-c306684b328d","name":"6:panel_6","type":"visualization"},{"id":"b8e35c80-623c-11eb-aebf-c306684b328d","name":"7:panel_7","type":"visualization"},{"id":"f1bc75d0-6239-11eb-aebf-c306684b328d","name":"8:panel_8","type":"visualization"},{"id":"0d8a8860-623a-11eb-aebf-c306684b328d","name":"9:panel_9","type":"visualization"},{"id":"d79fe3d0-6239-11eb-aebf-c306684b328d","name":"10:panel_10","type":"visualization"},{"id":"318375a0-6240-11eb-aebf-c306684b328d","name":"11:panel_11","type":"visualization"},{"id":"e461eb20-6245-11eb-aebf-c306684b328d","name":"12:panel_12","type":"visualization"},{"id":"25bdc750-6242-11eb-aebf-c306684b328d","name":"13:panel_13","type":"visualization"},{"id":"71dd7bc0-6248-11eb-aebf-c306684b328d","name":"14:panel_14","type":"visualization"},{"id":"6aea48a0-6240-11eb-aebf-c306684b328d","name":"15:panel_15","type":"visualization"},{"id":"32b681f0-6241-11eb-aebf-c306684b328d","name":"16:panel_16","type":"visualization"},{"id":"ccca99e0-6244-11eb-aebf-c306684b328d","name":"17:panel_17","type":"visualization"},{"id":"a4d7be80-6245-11eb-aebf-c306684b328d","name":"18:panel_18","type":"visualization"},{"id":"c94d8440-6248-11eb-aebf-c306684b328d","name":"19:panel_19","type":"visualization"},{"id":"db6226f0-61c0-11eb-aebf-c306684b328d","name":"20:panel_20","type":"search"}],"sort":[1623693556928,515],"type":"dashboard","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NzAsNF0="} -{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"26e2cf99-d931-4320-9e15-9dbc148f3534":{"columnOrder":["6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e","beb72af1-239c-46d8-823b-b00d1e2ace43"],"columns":{"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e":{"dataType":"string","isBucketed":true,"label":"Top values of url.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"beb72af1-239c-46d8-823b-b00d1e2ace43","type":"column"},"orderDirection":"desc","otherBucket":true,"size":20},"scale":"ordinal","sourceField":"url.raw"},"beb72af1-239c-46d8-823b-b00d1e2ace43":{"dataType":"number","isBucketed":false,"label":"Unique count of geo.srcdest","operationType":"unique_count","scale":"ratio","sourceField":"geo.srcdest"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"layers":[{"categoryDisplay":"default","groups":["6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e","6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e","6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e"],"layerId":"26e2cf99-d931-4320-9e15-9dbc148f3534","legendDisplay":"default","metric":"beb72af1-239c-46d8-823b-b00d1e2ace43","nestedLegend":false,"numberDisplay":"percent"}],"shape":"donut"}},"title":"lens_pie_chart","visualizationType":"lnsPie"},"coreMigrationVersion":"7.13.2","id":"21905950-bd9f-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-26e2cf99-d931-4320-9e15-9dbc148f3534","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,519],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NzEsNF0="} -{"attributes":{"description":null,"state":{"datasourceStates":{"indexpattern":{"layers":{"a3ac0e3d-63ec-49b2-882a-b34680a967ba":{"columnOrder":["352a2c02-aa6f-4a35-b776-45c3715a6c5e","8ef68cbb-e039-49d6-b15e-be81559f4b55","14fad6b1-6a7c-4ae8-ae4b-d9569e31e04a"],"columns":{"14fad6b1-6a7c-4ae8-ae4b-d9569e31e04a":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"352a2c02-aa6f-4a35-b776-45c3715a6c5e":{"dataType":"string","isBucketed":true,"label":"Top values of geo.srcdest","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"14fad6b1-6a7c-4ae8-ae4b-d9569e31e04a","type":"column"},"orderDirection":"desc","otherBucket":true,"size":67},"scale":"ordinal","sourceField":"geo.srcdest"},"8ef68cbb-e039-49d6-b15e-be81559f4b55":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["14fad6b1-6a7c-4ae8-ae4b-d9569e31e04a"],"layerId":"a3ac0e3d-63ec-49b2-882a-b34680a967ba","position":"top","seriesType":"bar_percentage_stacked","showGridlines":false,"splitAccessor":"352a2c02-aa6f-4a35-b776-45c3715a6c5e","xAccessor":"8ef68cbb-e039-49d6-b15e-be81559f4b55"}],"legend":{"isVisible":true,"position":"top","showSingleSeries":true},"preferredSeriesType":"bar_percentage_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_bar_verticalpercentage","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.2","id":"aa4b8da0-bd9f-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-a3ac0e3d-63ec-49b2-882a-b34680a967ba","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,523],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NzIsNF0="} -{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"037b7937-790b-4d2d-94a5-7f5837a6ef05":{"columnOrder":["b3d46616-75e0-419e-97ea-91148961ef94","025a0fb3-dc44-4f5c-b517-2d71d3f26f14","c476db14-0cc1-40ec-863e-d2779256a407"],"columns":{"025a0fb3-dc44-4f5c-b517-2d71d3f26f14":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"b3d46616-75e0-419e-97ea-91148961ef94":{"dataType":"string","isBucketed":true,"label":"Top values of geo.srcdest","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"c476db14-0cc1-40ec-863e-d2779256a407","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"geo.srcdest"},"c476db14-0cc1-40ec-863e-d2779256a407":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"lucene","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["c476db14-0cc1-40ec-863e-d2779256a407"],"layerId":"037b7937-790b-4d2d-94a5-7f5837a6ef05","position":"top","seriesType":"bar_stacked","showGridlines":false,"splitAccessor":"b3d46616-75e0-419e-97ea-91148961ef94","xAccessor":"025a0fb3-dc44-4f5c-b517-2d71d3f26f14"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_barchart_vertical","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.2","id":"2d3f1250-bd9f-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-037b7937-790b-4d2d-94a5-7f5837a6ef05","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,527],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NzMsNF0="} -{"attributes":{"description":null,"state":{"datasourceStates":{"indexpattern":{"layers":{"212688dc-e7d7-4875-a221-09e6191bdcf7":{"columnOrder":["05410186-83c4-460a-82bf-dd7e9d998c9f","e8659feb-1db4-4706-9147-ac1fd513a1ba","c9a32fd0-a465-44fb-8adc-b957fb72cad5"],"columns":{"05410186-83c4-460a-82bf-dd7e9d998c9f":{"dataType":"string","isBucketed":true,"label":"Top values of extension.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"c9a32fd0-a465-44fb-8adc-b957fb72cad5","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"extension.raw"},"c9a32fd0-a465-44fb-8adc-b957fb72cad5":{"dataType":"number","isBucketed":false,"label":"Average of bytes","operationType":"average","scale":"ratio","sourceField":"bytes"},"e8659feb-1db4-4706-9147-ac1fd513a1ba":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["c9a32fd0-a465-44fb-8adc-b957fb72cad5"],"layerId":"212688dc-e7d7-4875-a221-09e6191bdcf7","position":"top","seriesType":"bar_horizontal_stacked","showGridlines":false,"splitAccessor":"05410186-83c4-460a-82bf-dd7e9d998c9f","xAccessor":"e8659feb-1db4-4706-9147-ac1fd513a1ba"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_horizontal_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_barhorizontal_stacked","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.2","id":"edd5a560-bda4-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-212688dc-e7d7-4875-a221-09e6191bdcf7","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,531],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NzQsNF0="} -{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"7ab04fd4-04da-4023-8899-d94620193607":{"columnOrder":["0ab2d5f8-11f0-4b25-b8bb-3127a3b8d4c7","9eb851dd-31f6-481a-84d1-9ecce53a6ad2","f6b271a7-509b-4c37-b7b6-ac5be4bcb49a"],"columns":{"0ab2d5f8-11f0-4b25-b8bb-3127a3b8d4c7":{"dataType":"string","isBucketed":true,"label":"Top values of request.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"f6b271a7-509b-4c37-b7b6-ac5be4bcb49a","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"request.raw"},"9eb851dd-31f6-481a-84d1-9ecce53a6ad2":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"f6b271a7-509b-4c37-b7b6-ac5be4bcb49a":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["f6b271a7-509b-4c37-b7b6-ac5be4bcb49a"],"layerId":"7ab04fd4-04da-4023-8899-d94620193607","position":"top","seriesType":"bar_horizontal_percentage_stacked","showGridlines":false,"splitAccessor":"0ab2d5f8-11f0-4b25-b8bb-3127a3b8d4c7","xAccessor":"9eb851dd-31f6-481a-84d1-9ecce53a6ad2"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_horizontal_percentage_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_barhorizontalpercentage","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.2","id":"2c25a450-bda5-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-7ab04fd4-04da-4023-8899-d94620193607","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,535],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NzUsNF0="} -{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"037b7937-790b-4d2d-94a5-7f5837a6ef05":{"columnOrder":["b3d46616-75e0-419e-97ea-91148961ef94","025a0fb3-dc44-4f5c-b517-2d71d3f26f14","c476db14-0cc1-40ec-863e-d2779256a407"],"columns":{"025a0fb3-dc44-4f5c-b517-2d71d3f26f14":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"b3d46616-75e0-419e-97ea-91148961ef94":{"dataType":"string","isBucketed":true,"label":"Top values of geo.srcdest","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"c476db14-0cc1-40ec-863e-d2779256a407","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"geo.srcdest"},"c476db14-0cc1-40ec-863e-d2779256a407":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"lucene","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["c476db14-0cc1-40ec-863e-d2779256a407"],"layerId":"037b7937-790b-4d2d-94a5-7f5837a6ef05","position":"top","seriesType":"bar_stacked","showGridlines":false,"splitAccessor":"b3d46616-75e0-419e-97ea-91148961ef94","xAccessor":"025a0fb3-dc44-4f5c-b517-2d71d3f26f14"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_visualization","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.2","id":"e79116e0-bd9e-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-037b7937-790b-4d2d-94a5-7f5837a6ef05","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693677171,767],"type":"lens","updated_at":"2021-06-14T18:01:17.171Z","version":"WzE3NDEsNF0="} -{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"72783e5f-aa7b-4b8a-b26c-a3e4d051340e":{"columnOrder":["0f619652-9ff1-453b-ae1f-7371baa82f55"],"columns":{"0f619652-9ff1-453b-ae1f-7371baa82f55":{"dataType":"number","isBucketed":false,"label":"Average of phpmemory","operationType":"average","params":{"format":{"id":"percent","params":{"decimals":10}}},"scale":"ratio","sourceField":"phpmemory"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"accessor":"0f619652-9ff1-453b-ae1f-7371baa82f55","layerId":"72783e5f-aa7b-4b8a-b26c-a3e4d051340e"}},"title":"lens_metric","visualizationType":"lnsMetric"},"coreMigrationVersion":"7.13.2","id":"974fb950-bda5-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-72783e5f-aa7b-4b8a-b26c-a3e4d051340e","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,543],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NzcsNF0="} -{"attributes":{"description":null,"state":{"datasourceStates":{"indexpattern":{"layers":{"bb478774-f9e8-4380-bf3a-f4a89a4d79b5":{"columnOrder":["4573ae8f-8f9d-4918-b496-c08f7102c6e1","cebdc6c5-3587-4f57-879c-dd63ea99cf03"],"columns":{"4573ae8f-8f9d-4918-b496-c08f7102c6e1":{"dataType":"string","isBucketed":true,"label":"Top values of machine.os.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"cebdc6c5-3587-4f57-879c-dd63ea99cf03","type":"column"},"orderDirection":"desc","otherBucket":true,"size":5},"scale":"ordinal","sourceField":"machine.os.raw"},"cebdc6c5-3587-4f57-879c-dd63ea99cf03":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"layers":[{"categoryDisplay":"default","groups":["4573ae8f-8f9d-4918-b496-c08f7102c6e1"],"layerId":"bb478774-f9e8-4380-bf3a-f4a89a4d79b5","legendDisplay":"default","metric":"cebdc6c5-3587-4f57-879c-dd63ea99cf03","nestedLegend":false,"numberDisplay":"percent"}],"shape":"pie"}},"title":"lens_piechart","visualizationType":"lnsPie"},"coreMigrationVersion":"7.13.2","id":"51b63040-bda5-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-bb478774-f9e8-4380-bf3a-f4a89a4d79b5","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,547],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NzgsNF0="} -{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"a1b85651-db29-441f-8f08-cf1b9b6f7bf1":{"columnOrder":["2b3bdc32-0be0-49dc-993d-4630b0bd1185","b85cc0a7-0b18-4b08-b7f0-c617f80cf903","03203126-8286-444d-b5b3-4f399eaf2c26","44305317-61e8-4600-9f3c-ac4070e0c529"],"columns":{"03203126-8286-444d-b5b3-4f399eaf2c26":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"2b3bdc32-0be0-49dc-993d-4630b0bd1185":{"dataType":"string","isBucketed":true,"label":"Top values of extension.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"44305317-61e8-4600-9f3c-ac4070e0c529","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"extension.raw"},"44305317-61e8-4600-9f3c-ac4070e0c529":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"b85cc0a7-0b18-4b08-b7f0-c617f80cf903":{"dataType":"string","isBucketed":true,"label":"Top values of machine.os.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"44305317-61e8-4600-9f3c-ac4070e0c529","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"machine.os.raw"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"columns":[{"columnId":"2b3bdc32-0be0-49dc-993d-4630b0bd1185","isTransposed":false},{"columnId":"b85cc0a7-0b18-4b08-b7f0-c617f80cf903","isTransposed":false},{"columnId":"03203126-8286-444d-b5b3-4f399eaf2c26","isTransposed":false},{"columnId":"44305317-61e8-4600-9f3c-ac4070e0c529","isTransposed":false}],"layerId":"a1b85651-db29-441f-8f08-cf1b9b6f7bf1"}},"title":"lens_table","visualizationType":"lnsDatatable"},"coreMigrationVersion":"7.13.2","id":"b00679c0-bda5-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-a1b85651-db29-441f-8f08-cf1b9b6f7bf1","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,551],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2NzksNF0="} -{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"4fbb51e9-1f99-4b5e-b59d-60fcb547b1d9":{"columnOrder":["08a1af05-743d-480e-9056-3405b1bdda7d","bae35990-75c2-487f-94eb-d8e03d2eda33"],"columns":{"08a1af05-743d-480e-9056-3405b1bdda7d":{"dataType":"string","isBucketed":true,"label":"Top values of geo.srcdest","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"bae35990-75c2-487f-94eb-d8e03d2eda33","type":"column"},"orderDirection":"desc","otherBucket":true,"size":25},"scale":"ordinal","sourceField":"geo.srcdest"},"bae35990-75c2-487f-94eb-d8e03d2eda33":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"layers":[{"categoryDisplay":"default","groups":["08a1af05-743d-480e-9056-3405b1bdda7d","08a1af05-743d-480e-9056-3405b1bdda7d","08a1af05-743d-480e-9056-3405b1bdda7d"],"layerId":"4fbb51e9-1f99-4b5e-b59d-60fcb547b1d9","legendDisplay":"default","metric":"bae35990-75c2-487f-94eb-d8e03d2eda33","nestedLegend":false,"numberDisplay":"percent"}],"shape":"treemap"}},"title":"lens_treemap","visualizationType":"lnsPie"},"coreMigrationVersion":"7.13.2","id":"652ade10-bd9f-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-4fbb51e9-1f99-4b5e-b59d-60fcb547b1d9","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,555],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2ODAsNF0="} -{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"e84503c1-4dbd-4ac6-9ac9-ad938654680f":{"columnOrder":["38c73fd4-6330-4162-8a7b-1a059f005da8","e8d4dad2-ac30-4741-aca0-904eb1fc8455","70433aa7-3c2c-4e6c-b8cf-4218c995cff5"],"columns":{"38c73fd4-6330-4162-8a7b-1a059f005da8":{"dataType":"string","isBucketed":true,"label":"Top values of url.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"70433aa7-3c2c-4e6c-b8cf-4218c995cff5","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"url.raw"},"70433aa7-3c2c-4e6c-b8cf-4218c995cff5":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"e8d4dad2-ac30-4741-aca0-904eb1fc8455":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["70433aa7-3c2c-4e6c-b8cf-4218c995cff5"],"layerId":"e84503c1-4dbd-4ac6-9ac9-ad938654680f","position":"top","seriesType":"line","showGridlines":false,"splitAccessor":"38c73fd4-6330-4162-8a7b-1a059f005da8","xAccessor":"e8d4dad2-ac30-4741-aca0-904eb1fc8455"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"line","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_line_chart","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.2","id":"7f3b5fb0-be2f-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-e84503c1-4dbd-4ac6-9ac9-ad938654680f","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,559],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2ODEsNF0="} -{"attributes":{"fieldAttrs":"{\"speaker\":{\"count\":1},\"text_entry\":{\"count\":6},\"type\":{\"count\":3}}","fields":"[]","runtimeFieldMap":"{}","title":"shakespeare"},"coreMigrationVersion":"7.13.2","id":"4e937b20-619d-11eb-aebf-c306684b328d","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"sort":[1623693556928,560],"type":"index-pattern","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2ODIsNF0="} -{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"d35680ce-c285-4fae-89d6-1245671bbc78":{"columnOrder":["2bcbffbe-c24d-4e74-8a03-9a6da7db70c0","6b00fde6-bfaa-4da1-beeb-bfd85a4cb2ff","8319857d-a03b-4158-bdf1-2a788e510445"],"columns":{"2bcbffbe-c24d-4e74-8a03-9a6da7db70c0":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"6b00fde6-bfaa-4da1-beeb-bfd85a4cb2ff":{"dataType":"number","isBucketed":false,"label":"Average of bytes","operationType":"average","scale":"ratio","sourceField":"bytes"},"8319857d-a03b-4158-bdf1-2a788e510445":{"dataType":"number","isBucketed":false,"label":"Sum of bytes_scripted","operationType":"sum","params":{"format":{"id":"number","params":{"decimals":2}}},"scale":"ratio","sourceField":"bytes_scripted"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["6b00fde6-bfaa-4da1-beeb-bfd85a4cb2ff","8319857d-a03b-4158-bdf1-2a788e510445"],"layerId":"d35680ce-c285-4fae-89d6-1245671bbc78","position":"top","seriesType":"area","showGridlines":false,"xAccessor":"2bcbffbe-c24d-4e74-8a03-9a6da7db70c0","yConfig":[{"axisMode":"auto","forAccessor":"8319857d-a03b-4158-bdf1-2a788e510445"}]}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"area","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_area_chart","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.2","id":"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-d35680ce-c285-4fae-89d6-1245671bbc78","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,564],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2ODMsNF0="} -{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"70bd567e-8e67-4696-a406-313b06344fa9":{"columnOrder":["96ddedfb-043b-479e-a746-600e72ab546e","d325b7da-4266-4035-9b13-5f853615149a","2fc1391b-17d1-4c49-9ddc-06ff307e3520","1cc6f19c-cbcb-4abd-b56d-1a2f9deae5f3"],"columns":{"1cc6f19c-cbcb-4abd-b56d-1a2f9deae5f3":{"dataType":"number","isBucketed":false,"label":"Average of machine.ram","operationType":"average","scale":"ratio","sourceField":"machine.ram"},"2fc1391b-17d1-4c49-9ddc-06ff307e3520":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"96ddedfb-043b-479e-a746-600e72ab546e":{"dataType":"string","isBucketed":true,"label":"Top values of machine.os.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"2fc1391b-17d1-4c49-9ddc-06ff307e3520","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"machine.os.raw"},"d325b7da-4266-4035-9b13-5f853615149a":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["2fc1391b-17d1-4c49-9ddc-06ff307e3520","1cc6f19c-cbcb-4abd-b56d-1a2f9deae5f3"],"layerId":"70bd567e-8e67-4696-a406-313b06344fa9","position":"top","seriesType":"area_stacked","showGridlines":false,"splitAccessor":"96ddedfb-043b-479e-a746-600e72ab546e","xAccessor":"d325b7da-4266-4035-9b13-5f853615149a"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"area_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_area_stacked","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.2","id":"dd315430-be2f-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-70bd567e-8e67-4696-a406-313b06344fa9","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,568],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2ODQsNF0="} -{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"2e80716f-c1b6-46f2-be2b-35db744b5031\"},\"panelIndex\":\"2e80716f-c1b6-46f2-be2b-35db744b5031\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsPie\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"26e2cf99-d931-4320-9e15-9dbc148f3534\":{\"columns\":{\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\":{\"label\":\"Top values of url.raw\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"url.raw\",\"isBucketed\":true,\"params\":{\"size\":20,\"orderBy\":{\"type\":\"column\",\"columnId\":\"beb72af1-239c-46d8-823b-b00d1e2ace43\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"beb72af1-239c-46d8-823b-b00d1e2ace43\":{\"label\":\"Unique count of geo.srcdest\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"geo.srcdest\",\"isBucketed\":false}},\"columnOrder\":[\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"beb72af1-239c-46d8-823b-b00d1e2ace43\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"shape\":\"donut\",\"layers\":[{\"layerId\":\"26e2cf99-d931-4320-9e15-9dbc148f3534\",\"groups\":[\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\"],\"metric\":\"beb72af1-239c-46d8-823b-b00d1e2ace43\",\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"default\",\"nestedLegend\":false}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-26e2cf99-d931-4320-9e15-9dbc148f3534\"}]},\"enhancements\":{},\"type\":\"lens\"},\"panelRefName\":\"panel_2e80716f-c1b6-46f2-be2b-35db744b5031\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"da8843e0-6789-4aae-bcd0-81f270538719\"},\"panelIndex\":\"da8843e0-6789-4aae-bcd0-81f270538719\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_da8843e0-6789-4aae-bcd0-81f270538719\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"adcd4418-7299-4efa-b369-5f71a7b4ebe0\"},\"panelIndex\":\"adcd4418-7299-4efa-b369-5f71a7b4ebe0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_adcd4418-7299-4efa-b369-5f71a7b4ebe0\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"869754a7-edf0-478f-a7f1-80374f63108a\"},\"panelIndex\":\"869754a7-edf0-478f-a7f1-80374f63108a\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_869754a7-edf0-478f-a7f1-80374f63108a\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":30,\"w\":24,\"h\":15,\"i\":\"67111cf4-338e-453f-8621-e8dea64082d1\"},\"panelIndex\":\"67111cf4-338e-453f-8621-e8dea64082d1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_67111cf4-338e-453f-8621-e8dea64082d1\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\"},\"panelIndex\":\"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":45,\"w\":24,\"h\":15,\"i\":\"88847944-ae1b-45fd-b102-3b45f9bea04b\"},\"panelIndex\":\"88847944-ae1b-45fd-b102-3b45f9bea04b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_88847944-ae1b-45fd-b102-3b45f9bea04b\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":45,\"w\":24,\"h\":15,\"i\":\"5a7924c7-eac0-4573-9199-fecec5b82e9e\"},\"panelIndex\":\"5a7924c7-eac0-4573-9199-fecec5b82e9e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5a7924c7-eac0-4573-9199-fecec5b82e9e\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":60,\"w\":24,\"h\":15,\"i\":\"f8f49591-f071-4a96-b1ed-cd65daff5648\"},\"panelIndex\":\"f8f49591-f071-4a96-b1ed-cd65daff5648\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_f8f49591-f071-4a96-b1ed-cd65daff5648\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":60,\"w\":24,\"h\":15,\"i\":\"9f357f47-c2a0-421f-a456-9583c40837ab\"},\"panelIndex\":\"9f357f47-c2a0-421f-a456-9583c40837ab\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9f357f47-c2a0-421f-a456-9583c40837ab\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":75,\"w\":24,\"h\":15,\"i\":\"6cb383e9-1e80-44f9-80d5-7b8c585668db\"},\"panelIndex\":\"6cb383e9-1e80-44f9-80d5-7b8c585668db\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6cb383e9-1e80-44f9-80d5-7b8c585668db\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":75,\"w\":24,\"h\":15,\"i\":\"57f5f0bf-6610-4599-aad4-37484640b5e2\"},\"panelIndex\":\"57f5f0bf-6610-4599-aad4-37484640b5e2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_57f5f0bf-6610-4599-aad4-37484640b5e2\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":90,\"w\":24,\"h\":15,\"i\":\"32d3ab66-52e1-44e3-8c1f-1dccff3c5692\"},\"panelIndex\":\"32d3ab66-52e1-44e3-8c1f-1dccff3c5692\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_32d3ab66-52e1-44e3-8c1f-1dccff3c5692\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":90,\"w\":24,\"h\":15,\"i\":\"dd1718fd-74ee-4032-851b-db97e893825d\"},\"panelIndex\":\"dd1718fd-74ee-4032-851b-db97e893825d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_dd1718fd-74ee-4032-851b-db97e893825d\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":105,\"w\":24,\"h\":15,\"i\":\"98a556ee-078b-4e03-93a8-29996133cdcb\"},\"panelIndex\":\"98a556ee-078b-4e03-93a8-29996133cdcb\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"999a2d60-cb2a-451c-8d71-80d7e92e70fd\":{\"columns\":{\"ce9117a2-773c-474c-8fb1-18940cf58b38\":{\"label\":\"Top values of type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"type\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\"},\"orderDirection\":\"asc\",\"otherBucket\":true,\"missingBucket\":false}},\"a3d10552-e352-40d0-a156-e86112c0501a\":{\"label\":\"Top values of _type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"_type\",\"isBucketed\":true,\"params\":{\"size\":3,\"orderBy\":{\"type\":\"column\",\"columnId\":\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"},\"9c5db2f3-9eb0-4667-9a74-3318301de251\":{\"label\":\"Sum of bytes\",\"dataType\":\"number\",\"operationType\":\"sum\",\"sourceField\":\"bytes\",\"isBucketed\":false,\"scale\":\"ratio\"}},\"columnOrder\":[\"ce9117a2-773c-474c-8fb1-18940cf58b38\",\"a3d10552-e352-40d0-a156-e86112c0501a\",\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\",\"9c5db2f3-9eb0-4667-9a74-3318301de251\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"999a2d60-cb2a-451c-8d71-80d7e92e70fd\",\"accessors\":[\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\",\"9c5db2f3-9eb0-4667-9a74-3318301de251\"],\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"xAccessor\":\"ce9117a2-773c-474c-8fb1-18940cf58b38\",\"splitAccessor\":\"a3d10552-e352-40d0-a156-e86112c0501a\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-999a2d60-cb2a-451c-8d71-80d7e92e70fd\"}]},\"enhancements\":{},\"type\":\"lens\"}},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":105,\"w\":24,\"h\":15,\"i\":\"62a0f0b0-3589-4cef-807b-b1b4258b7a9b\"},\"panelIndex\":\"62a0f0b0-3589-4cef-807b-b1b4258b7a9b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_62a0f0b0-3589-4cef-807b-b1b4258b7a9b\"},{\"version\":\"7.13.1\",\"type\":\"map\",\"gridData\":{\"x\":0,\"y\":120,\"w\":24,\"h\":15,\"i\":\"dcc0defa-3376-465c-9b5b-2ba69528848c\"},\"panelIndex\":\"dcc0defa-3376-465c-9b5b-2ba69528848c\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":19.94277,\"lon\":0,\"zoom\":1.56},\"mapBuffer\":{\"minLon\":-210.32666,\"minLat\":-64.8435,\"maxLon\":210.32666,\"maxLat\":95.13806},\"isLayerTOCOpen\":true,\"openTOCDetails\":[],\"hiddenLayers\":[],\"enhancements\":{}},\"panelRefName\":\"panel_dcc0defa-3376-465c-9b5b-2ba69528848c\"}]","refreshInterval":{"pause":true,"value":0},"timeFrom":"2015-09-20T01:56:56.132Z","timeRestore":true,"timeTo":"2015-09-21T11:18:20.471Z","title":"lens_maps_dashboard_logstash","version":1},"coreMigrationVersion":"7.13.2","id":"16d86080-be5c-11eb-9520-1b4c3ca6a781","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"21905950-bd9f-11eb-9520-1b4c3ca6a781","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:panel_2e80716f-c1b6-46f2-be2b-35db744b5031","type":"lens"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:indexpattern-datasource-layer-26e2cf99-d931-4320-9e15-9dbc148f3534","type":"index-pattern"},{"id":"aa4b8da0-bd9f-11eb-9520-1b4c3ca6a781","name":"da8843e0-6789-4aae-bcd0-81f270538719:panel_da8843e0-6789-4aae-bcd0-81f270538719","type":"lens"},{"id":"2d3f1250-bd9f-11eb-9520-1b4c3ca6a781","name":"adcd4418-7299-4efa-b369-5f71a7b4ebe0:panel_adcd4418-7299-4efa-b369-5f71a7b4ebe0","type":"lens"},{"id":"edd5a560-bda4-11eb-9520-1b4c3ca6a781","name":"869754a7-edf0-478f-a7f1-80374f63108a:panel_869754a7-edf0-478f-a7f1-80374f63108a","type":"lens"},{"id":"2c25a450-bda5-11eb-9520-1b4c3ca6a781","name":"67111cf4-338e-453f-8621-e8dea64082d1:panel_67111cf4-338e-453f-8621-e8dea64082d1","type":"lens"},{"id":"e79116e0-bd9e-11eb-9520-1b4c3ca6a781","name":"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d:panel_13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d","type":"lens"},{"id":"974fb950-bda5-11eb-9520-1b4c3ca6a781","name":"88847944-ae1b-45fd-b102-3b45f9bea04b:panel_88847944-ae1b-45fd-b102-3b45f9bea04b","type":"lens"},{"id":"21905950-bd9f-11eb-9520-1b4c3ca6a781","name":"5a7924c7-eac0-4573-9199-fecec5b82e9e:panel_5a7924c7-eac0-4573-9199-fecec5b82e9e","type":"lens"},{"id":"51b63040-bda5-11eb-9520-1b4c3ca6a781","name":"f8f49591-f071-4a96-b1ed-cd65daff5648:panel_f8f49591-f071-4a96-b1ed-cd65daff5648","type":"lens"},{"id":"b00679c0-bda5-11eb-9520-1b4c3ca6a781","name":"9f357f47-c2a0-421f-a456-9583c40837ab:panel_9f357f47-c2a0-421f-a456-9583c40837ab","type":"lens"},{"id":"652ade10-bd9f-11eb-9520-1b4c3ca6a781","name":"6cb383e9-1e80-44f9-80d5-7b8c585668db:panel_6cb383e9-1e80-44f9-80d5-7b8c585668db","type":"lens"},{"id":"7f3b5fb0-be2f-11eb-9520-1b4c3ca6a781","name":"57f5f0bf-6610-4599-aad4-37484640b5e2:panel_57f5f0bf-6610-4599-aad4-37484640b5e2","type":"lens"},{"id":"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781","name":"32d3ab66-52e1-44e3-8c1f-1dccff3c5692:panel_32d3ab66-52e1-44e3-8c1f-1dccff3c5692","type":"lens"},{"id":"dd315430-be2f-11eb-9520-1b4c3ca6a781","name":"dd1718fd-74ee-4032-851b-db97e893825d:panel_dd1718fd-74ee-4032-851b-db97e893825d","type":"lens"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"98a556ee-078b-4e03-93a8-29996133cdcb:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"98a556ee-078b-4e03-93a8-29996133cdcb:indexpattern-datasource-layer-999a2d60-cb2a-451c-8d71-80d7e92e70fd","type":"index-pattern"},{"id":"0dbbf8b0-be3c-11eb-9520-1b4c3ca6a781","name":"62a0f0b0-3589-4cef-807b-b1b4258b7a9b:panel_62a0f0b0-3589-4cef-807b-b1b4258b7a9b","type":"lens"},{"id":"0c5974f0-be5c-11eb-9520-1b4c3ca6a781","name":"dcc0defa-3376-465c-9b5b-2ba69528848c:panel_dcc0defa-3376-465c-9b5b-2ba69528848c","type":"map"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,590],"type":"dashboard","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2ODUsNF0="} -{"attributes":{"fieldAttrs":"{}","fields":"[]","runtimeFieldMap":"{}","title":".kibana"},"coreMigrationVersion":"7.13.2","id":"1773aa90-be66-11eb-9520-1b4c3ca6a781","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"sort":[1623693556928,591],"type":"index-pattern","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2ODYsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"shakespeare_areachart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"shakespeare_areachart\",\"type\":\"histogram\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"mode\":\"stacked\",\"type\":\"histogram\",\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"data\":{\"id\":\"2\",\"label\":\"Count\"},\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true},\"aggs\":[{\"id\":\"2\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"play_name\",\"size\":20,\"order\":\"desc\",\"orderBy\":\"2\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"play_name\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"2\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.2","id":"185283c0-619e-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,593],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2ODcsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"by_reference_logstash","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"by_reference_logstash\",\"type\":\"histogram\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{},\"style\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"},\"style\":{}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true}],\"radiusRatio\":0,\"addTooltip\":true,\"detailedTooltip\":true,\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"labels\":{\"show\":false},\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2014-07-15T12:33:21.084Z\",\"to\":\"2019-01-28T03:18:12.440Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}},\"schema\":\"segment\"},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"response.raw\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"group\"}]}"},"coreMigrationVersion":"7.13.2","id":"1885abb0-ca2b-11eb-bf5e-3de94e83d4f0","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623415891791,140],"type":"visualization","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0MzgsNF0="} -{"attributes":{"color":"#f44fcf","description":"","name":"shakespeare"},"coreMigrationVersion":"7.13.2","id":"42b4cec0-be32-11eb-9520-1b4c3ca6a781","references":[],"sort":[1623693556928,594],"type":"tag","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2ODgsNF0="} -{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"3338dd55-4007-4be5-908d-25722b6174cb":{"columnOrder":["6c83b0c2-5834-4619-888c-9e8a08e47d42","b25e7497-c188-4c25-b002-1fd5bd69e76d"],"columns":{"6c83b0c2-5834-4619-888c-9e8a08e47d42":{"dataType":"string","isBucketed":true,"label":"Top values of speaker","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"b25e7497-c188-4c25-b002-1fd5bd69e76d","type":"column"},"orderDirection":"desc","otherBucket":false,"size":90},"scale":"ordinal","sourceField":"speaker"},"b25e7497-c188-4c25-b002-1fd5bd69e76d":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"layers":[{"categoryDisplay":"default","groups":["6c83b0c2-5834-4619-888c-9e8a08e47d42","6c83b0c2-5834-4619-888c-9e8a08e47d42","6c83b0c2-5834-4619-888c-9e8a08e47d42"],"layerId":"3338dd55-4007-4be5-908d-25722b6174cb","legendDisplay":"default","metric":"b25e7497-c188-4c25-b002-1fd5bd69e76d","nestedLegend":false,"numberDisplay":"percent"}],"palette":{"name":"complimentary","type":"palette"},"shape":"treemap"}},"title":"lens_shakespeare_treemap","visualizationType":"lnsPie"},"coreMigrationVersion":"7.13.2","id":"31e9f2f0-be32-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-3338dd55-4007-4be5-908d-25722b6174cb","type":"index-pattern"},{"id":"42b4cec0-be32-11eb-9520-1b4c3ca6a781","name":"tag-ref-42b4cec0-be32-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,598],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2ODksNF0="} -{"attributes":{"accessCount":0,"accessDate":1622059178542,"createDate":1622059178542,"url":"/app/dashboards#/view/73398a90-619e-11eb-aebf-c306684b328d?embed=true&_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:%272015-09-20T01:56:56.132Z%27,to:%272015-09-21T11:18:20.471Z%27))&_a=(description:%27%27,filters:!(),fullScreenMode:!f,options:(darkTheme:!f,hidePanelTitles:!f,useMargins:!t),panels:!((embeddableConfig:(enhancements:()),gridData:(h:15,i:%271%27,w:24,x:0,y:0),id:%27185283c0-619e-11eb-aebf-c306684b328d%27,panelIndex:%271%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%272%27,w:24,x:24,y:0),id:%2733736660-619e-11eb-aebf-c306684b328d%27,panelIndex:%272%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%273%27,w:24,x:0,y:15),id:%27622ac7f0-619e-11eb-aebf-c306684b328d%27,panelIndex:%273%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%274%27,w:24,x:24,y:15),id:%27712ebbe0-619d-11eb-aebf-c306684b328d%27,panelIndex:%274%27,type:search,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%275%27,w:24,x:0,y:30),id:ddacc820-619d-11eb-aebf-c306684b328d,panelIndex:%275%27,type:search,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%276%27,w:24,x:24,y:30),id:f852d570-619d-11eb-aebf-c306684b328d,panelIndex:%276%27,type:search,version:%277.13.1%27)),query:(language:kuery,query:%27%27),tags:!(),timeRestore:!f,title:shakespeare_dashboard,viewMode:view)"},"coreMigrationVersion":"7.13.2","id":"32a03249ec3a048108d4b5a427a37fc8","references":[],"sort":[1623693556928,599],"type":"url","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2OTAsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"shakespeare_piechart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"shakespeare_piechart\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":false,\"labels\":{\"show\":true,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"play_name\",\"size\":15,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.2","id":"33736660-619e-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,601],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2OTEsNF0="} -{"attributes":{"color":"#7b01cf","description":"","name":"By reference"},"coreMigrationVersion":"7.13.2","id":"39d2c190-ca2b-11eb-bf5e-3de94e83d4f0","references":[],"sort":[1623415891791,147],"type":"tag","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0NDIsNF0="} -{"attributes":{"fieldAttrs":"{}","fields":"[]","runtimeFieldMap":"{}","title":"shakespeare"},"coreMigrationVersion":"7.13.2","id":"39d52f60-ca27-11eb-bf5e-3de94e83d4f0","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"sort":[1623415891791,148],"type":"index-pattern","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0NDMsNF0="} -{"attributes":{"columns":[],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[{\"meta\":{\"alias\":null,\"negate\":false,\"disabled\":false,\"type\":\"phrase\",\"key\":\"ip\",\"params\":{\"query\":\"57.237.11.219\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match_phrase\":{\"ip\":\"57.237.11.219\"}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["@timestamp","desc"]],"title":"drilldown_saved_search","version":1},"coreMigrationVersion":"7.13.2","id":"4acce030-ca2a-11eb-bf5e-3de94e83d4f0","migrationVersion":{"search":"7.9.3"},"references":[{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"}],"sort":[1623415891791,151],"type":"search","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0NDQsNF0="} -{"attributes":{"description":"","layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"e0d51731-2bb3-4fed-92af-65f93c3e7e58\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"},{\"sourceDescriptor\":{\"geoField\":\"geo.coordinates\",\"filterByMapBounds\":true,\"scalingType\":\"CLUSTERS\",\"topHitsSplitField\":\"\",\"topHitsSize\":1,\"id\":\"142e0a6b-53c9-4f66-a65d-fced755318de\",\"type\":\"ES_SEARCH\",\"applyGlobalQuery\":true,\"applyGlobalTime\":true,\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"id\":\"ca96ce4a-4e73-46a5-bcc8-99a39d227030\",\"label\":null,\"minZoom\":9,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#CA8EAE\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#934193\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true},\"type\":\"BLENDED_VECTOR\",\"joins\":[]}]","mapStateJSON":"{\"zoom\":1.38,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"2014-07-15T12:33:21.084Z\",\"to\":\"2019-01-28T03:18:12.440Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"backgroundColor\":\"#ffffff\",\"disableInteractive\":false,\"disableTooltipControl\":false,\"hideToolbarOverlay\":false,\"hideLayerControl\":false,\"hideViewControl\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showScaleControl\":false,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}","title":"Logstash_map_by_reference","uiStateJSON":"{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}"},"coreMigrationVersion":"7.13.2","id":"a53a2db0-ca2b-11eb-bf5e-3de94e83d4f0","migrationVersion":{"map":"7.12.0"},"references":[{"id":"39d2c190-ca2b-11eb-bf5e-3de94e83d4f0","name":"tag-ref-39d2c190-ca2b-11eb-bf5e-3de94e83d4f0","type":"tag"},{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"layer_1_source_index_pattern","type":"index-pattern"}],"sort":[1623415891791,154],"type":"map","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0NDUsNF0="} -{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.12.1\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"7c29a321-2a9a-412b-9ed1-1d0a1f66ea63\"},\"panelIndex\":\"7c29a321-2a9a-412b-9ed1-1d0a1f66ea63\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"7.12.1\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"f2d1feb1-d807-46b1-90ac-96d4a9edb6b1\"},\"panelIndex\":\"f2d1feb1-d807-46b1-90ac-96d4a9edb6b1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.12.1\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"a3530107-8b1c-4e94-8f99-e239fa40a09c\"},\"panelIndex\":\"a3530107-8b1c-4e94-8f99-e239fa40a09c\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"b14188e0-53d6-433e-874f-b1be7c97487c\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"by_reference_going_to_value\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}},{\"eventId\":\"60cba413-1793-4dd3-b072-9d53655d5522\",\"triggers\":[\"SELECT_RANGE_TRIGGER\"],\"action\":{\"name\":\"Goto_Discover\",\"config\":{\"url\":{\"template\":\"http://localhost:5601/app/discover#/view/b3288100-ca2c-11eb-bf5e-3de94e83d4f0?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&_a=(columns:!(),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'43fcac20-ca27-11eb-bf5e-3de94e83d4f0',key:geo.dest,negate:!f,params:(query:US),type:phrase),query:(match_phrase:(geo.dest:US)))),index:'43fcac20-ca27-11eb-bf5e-3de94e83d4f0',interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))\"},\"openInNewTab\":true,\"encodeUrl\":true},\"factoryId\":\"URL_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.12.1\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"77245314-9495-4625-9f53-0946150e26d4\"},\"panelIndex\":\"77245314-9495-4625-9f53-0946150e26d4\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":19.94277,\"lon\":0,\"zoom\":1.38},\"mapBuffer\":{\"minLon\":-214.7723,\"minLat\":-74.644155,\"maxLon\":214.7723,\"maxLat\":102.864625},\"isLayerTOCOpen\":true,\"openTOCDetails\":[],\"hiddenLayers\":[],\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"9b61b9d4-20a3-4bca-9697-1097c524a943\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"By_reference_to_value\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_3\"}]","timeRestore":false,"title":"by_reference_drilldown","version":1},"coreMigrationVersion":"7.13.2","id":"3b844220-ca2b-11eb-bf5e-3de94e83d4f0","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"08dec860-ca29-11eb-bf5e-3de94e83d4f0","name":"drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:b14188e0-53d6-433e-874f-b1be7c97487c:dashboardId","type":"dashboard"},{"id":"35ce3b30-ca29-11eb-bf5e-3de94e83d4f0","name":"drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:9b61b9d4-20a3-4bca-9697-1097c524a943:dashboardId","type":"dashboard"},{"id":"4acce030-ca2a-11eb-bf5e-3de94e83d4f0","name":"panel_0","type":"search"},{"id":"0abce1c0-ca2a-11eb-bf5e-3de94e83d4f0","name":"panel_1","type":"search"},{"id":"1885abb0-ca2b-11eb-bf5e-3de94e83d4f0","name":"panel_2","type":"visualization"},{"id":"a53a2db0-ca2b-11eb-bf5e-3de94e83d4f0","name":"panel_3","type":"map"},{"id":"39d2c190-ca2b-11eb-bf5e-3de94e83d4f0","name":"tag-39d2c190-ca2b-11eb-bf5e-3de94e83d4f0","type":"tag"}],"sort":[1623415891791,162],"type":"dashboard","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0NDYsNF0="} -{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"a7a8f2fb-066e-4023-9755-821e84560b4a":{"columnOrder":["ee46f645-0af0-4b5d-8ed3-2557c98c9c12","91859a54-9b88-4478-8c80-0779fe165fba","62a4dea1-fab9-45ff-93e0-b99cfff719d5"],"columns":{"62a4dea1-fab9-45ff-93e0-b99cfff719d5":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"91859a54-9b88-4478-8c80-0779fe165fba":{"dataType":"string","isBucketed":true,"label":"Top values of play_name","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"62a4dea1-fab9-45ff-93e0-b99cfff719d5","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"play_name"},"ee46f645-0af0-4b5d-8ed3-2557c98c9c12":{"dataType":"string","isBucketed":true,"label":"Top values of speaker","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"62a4dea1-fab9-45ff-93e0-b99cfff719d5","type":"column"},"orderDirection":"desc","otherBucket":true,"size":25},"scale":"ordinal","sourceField":"speaker"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"layers":[{"categoryDisplay":"default","groups":["ee46f645-0af0-4b5d-8ed3-2557c98c9c12","ee46f645-0af0-4b5d-8ed3-2557c98c9c12","ee46f645-0af0-4b5d-8ed3-2557c98c9c12","ee46f645-0af0-4b5d-8ed3-2557c98c9c12","91859a54-9b88-4478-8c80-0779fe165fba"],"layerId":"a7a8f2fb-066e-4023-9755-821e84560b4a","legendDisplay":"default","metric":"62a4dea1-fab9-45ff-93e0-b99cfff719d5","nestedLegend":false,"numberDisplay":"percent"}],"palette":{"name":"kibana_palette","type":"palette"},"shape":"pie"}},"title":"lens_shakespeare_piechart","visualizationType":"lnsPie"},"coreMigrationVersion":"7.13.2","id":"b5bd5050-be31-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-a7a8f2fb-066e-4023-9755-821e84560b4a","type":"index-pattern"},{"id":"42b4cec0-be32-11eb-9520-1b4c3ca6a781","name":"tag-ref-42b4cec0-be32-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,605],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2OTIsNF0="} -{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"c4b1daae-a3af-4136-969e-8485d4ba53f9\"},\"panelIndex\":\"c4b1daae-a3af-4136-969e-8485d4ba53f9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_c4b1daae-a3af-4136-969e-8485d4ba53f9\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"f092b002-182e-49b8-bcc4-58f5233e041b\"},\"panelIndex\":\"f092b002-182e-49b8-bcc4-58f5233e041b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_f092b002-182e-49b8-bcc4-58f5233e041b\"}]","refreshInterval":{"pause":true,"value":0},"timeFrom":"2015-09-20T01:56:56.132Z","timeRestore":true,"timeTo":"2015-09-21T11:18:20.471Z","title":"lens_shakespeare_dashboard","version":1},"coreMigrationVersion":"7.13.2","id":"43fae350-be32-11eb-9520-1b4c3ca6a781","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"b5bd5050-be31-11eb-9520-1b4c3ca6a781","name":"c4b1daae-a3af-4136-969e-8485d4ba53f9:panel_c4b1daae-a3af-4136-969e-8485d4ba53f9","type":"lens"},{"id":"31e9f2f0-be32-11eb-9520-1b4c3ca6a781","name":"f092b002-182e-49b8-bcc4-58f5233e041b:panel_f092b002-182e-49b8-bcc4-58f5233e041b","type":"lens"},{"id":"42b4cec0-be32-11eb-9520-1b4c3ca6a781","name":"tag-42b4cec0-be32-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,609],"type":"dashboard","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2OTMsNF0="} -{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"3\",\"w\":24,\"x\":0,\"y\":15},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"4\",\"w\":24,\"x\":24,\"y\":15},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"5\",\"w\":24,\"x\":0,\"y\":30},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"6\",\"w\":24,\"x\":24,\"y\":30},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"7\",\"w\":24,\"x\":0,\"y\":45},\"panelIndex\":\"7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"8\",\"w\":24,\"x\":24,\"y\":45},\"panelIndex\":\"8\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"9\",\"w\":24,\"x\":0,\"y\":60},\"panelIndex\":\"9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"10\",\"w\":24,\"x\":24,\"y\":60},\"panelIndex\":\"10\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_10\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"11\",\"w\":24,\"x\":0,\"y\":75},\"panelIndex\":\"11\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_11\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"12\",\"w\":24,\"x\":24,\"y\":75},\"panelIndex\":\"12\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_12\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"13\",\"w\":24,\"x\":0,\"y\":90},\"panelIndex\":\"13\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"14\",\"w\":24,\"x\":24,\"y\":90},\"panelIndex\":\"14\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_14\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"15\",\"w\":24,\"x\":0,\"y\":105},\"panelIndex\":\"15\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_15\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"16\",\"w\":24,\"x\":24,\"y\":105},\"panelIndex\":\"16\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_16\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"17\",\"w\":24,\"x\":0,\"y\":120},\"panelIndex\":\"17\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_17\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"18\",\"w\":24,\"x\":24,\"y\":120},\"panelIndex\":\"18\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_18\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"19\",\"w\":24,\"x\":0,\"y\":135},\"panelIndex\":\"19\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_19\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"h\":15,\"i\":\"20\",\"w\":24,\"x\":24,\"y\":135},\"panelIndex\":\"20\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_20\"}]","timeRestore":false,"title":"logstash_dashboard_withouttime","version":1},"coreMigrationVersion":"7.13.2","id":"5d3410c0-6249-11eb-aebf-c306684b328d","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"36b91810-6239-11eb-aebf-c306684b328d","name":"1:panel_1","type":"visualization"},{"id":"0a274320-61cc-11eb-aebf-c306684b328d","name":"2:panel_2","type":"visualization"},{"id":"e4aef350-623d-11eb-aebf-c306684b328d","name":"3:panel_3","type":"visualization"},{"id":"f92e5630-623e-11eb-aebf-c306684b328d","name":"4:panel_4","type":"visualization"},{"id":"9853d4d0-623d-11eb-aebf-c306684b328d","name":"5:panel_5","type":"visualization"},{"id":"6ecb33b0-623d-11eb-aebf-c306684b328d","name":"6:panel_6","type":"visualization"},{"id":"b8e35c80-623c-11eb-aebf-c306684b328d","name":"7:panel_7","type":"visualization"},{"id":"f1bc75d0-6239-11eb-aebf-c306684b328d","name":"8:panel_8","type":"visualization"},{"id":"0d8a8860-623a-11eb-aebf-c306684b328d","name":"9:panel_9","type":"visualization"},{"id":"d79fe3d0-6239-11eb-aebf-c306684b328d","name":"10:panel_10","type":"visualization"},{"id":"318375a0-6240-11eb-aebf-c306684b328d","name":"11:panel_11","type":"visualization"},{"id":"e461eb20-6245-11eb-aebf-c306684b328d","name":"12:panel_12","type":"visualization"},{"id":"25bdc750-6242-11eb-aebf-c306684b328d","name":"13:panel_13","type":"visualization"},{"id":"71dd7bc0-6248-11eb-aebf-c306684b328d","name":"14:panel_14","type":"visualization"},{"id":"6aea48a0-6240-11eb-aebf-c306684b328d","name":"15:panel_15","type":"visualization"},{"id":"32b681f0-6241-11eb-aebf-c306684b328d","name":"16:panel_16","type":"visualization"},{"id":"ccca99e0-6244-11eb-aebf-c306684b328d","name":"17:panel_17","type":"visualization"},{"id":"a4d7be80-6245-11eb-aebf-c306684b328d","name":"18:panel_18","type":"visualization"},{"id":"c94d8440-6248-11eb-aebf-c306684b328d","name":"19:panel_19","type":"visualization"},{"id":"db6226f0-61c0-11eb-aebf-c306684b328d","name":"20:panel_20","type":"search"}],"sort":[1623693556928,630],"type":"dashboard","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2OTQsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"shakespeare_tag_cloud","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"shakespeare_tag_cloud\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"multiple\",\"minFontSize\":59,\"maxFontSize\":100,\"showLabel\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"type.keyword\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.2","id":"622ac7f0-619e-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,632],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2OTUsNF0="} -{"attributes":{"numLinks":4,"numVertices":5,"title":"logstash_graph","version":1,"wsState":"\"{\\\"selectedFields\\\":[{\\\"name\\\":\\\"machine.os.raw\\\",\\\"hopSize\\\":5,\\\"lastValidHopSize\\\":5,\\\"color\\\":\\\"#B9A888\\\",\\\"selected\\\":true,\\\"iconClass\\\":\\\"fa-folder-open-o\\\"},{\\\"name\\\":\\\"response.raw\\\",\\\"hopSize\\\":5,\\\"lastValidHopSize\\\":5,\\\"color\\\":\\\"#D6BF57\\\",\\\"selected\\\":true,\\\"iconClass\\\":\\\"fa-folder-open-o\\\"}],\\\"blocklist\\\":[],\\\"vertices\\\":[{\\\"x\\\":461.96184642905024,\\\"y\\\":284.02313214227325,\\\"label\\\":\\\"osx\\\",\\\"color\\\":\\\"#B9A888\\\",\\\"field\\\":\\\"machine.os.raw\\\",\\\"term\\\":\\\"osx\\\",\\\"parent\\\":null,\\\"size\\\":15},{\\\"x\\\":383.946159835112,\\\"y\\\":375.6063135315976,\\\"label\\\":\\\"503\\\",\\\"color\\\":\\\"#D6BF57\\\",\\\"field\\\":\\\"response.raw\\\",\\\"term\\\":\\\"503\\\",\\\"parent\\\":null,\\\"size\\\":15},{\\\"x\\\":287.104700756828,\\\"y\\\":324.1245253249895,\\\"label\\\":\\\"win 7\\\",\\\"color\\\":\\\"#B9A888\\\",\\\"field\\\":\\\"machine.os.raw\\\",\\\"term\\\":\\\"win 7\\\",\\\"parent\\\":null,\\\"size\\\":15},{\\\"x\\\":487.9986107998273,\\\"y\\\":407.07546535764254,\\\"label\\\":\\\"ios\\\",\\\"color\\\":\\\"#B9A888\\\",\\\"field\\\":\\\"machine.os.raw\\\",\\\"term\\\":\\\"ios\\\",\\\"parent\\\":null,\\\"size\\\":15},{\\\"x\\\":302.35059551806023,\\\"y\\\":211.66825720913607,\\\"label\\\":\\\"200\\\",\\\"color\\\":\\\"#D6BF57\\\",\\\"field\\\":\\\"response.raw\\\",\\\"term\\\":\\\"200\\\",\\\"parent\\\":null,\\\"size\\\":15}],\\\"links\\\":[{\\\"weight\\\":0.000881324009872165,\\\"width\\\":7.983523640193488,\\\"source\\\":4,\\\"target\\\":2},{\\\"weight\\\":0.000023386835221992895,\\\"width\\\":2,\\\"source\\\":1,\\\"target\\\":0},{\\\"weight\\\":0.0011039286029480653,\\\"width\\\":2,\\\"source\\\":1,\\\"target\\\":2},{\\\"weight\\\":0.000045596928960694605,\\\"width\\\":2,\\\"source\\\":1,\\\"target\\\":3}],\\\"urlTemplates\\\":[{\\\"url\\\":\\\"/app/discover#/?_a=(columns%3A!(_source)%2Cindex%3A%2756b34100-619d-11eb-aebf-c306684b328d%27%2Cinterval%3Aauto%2Cquery%3A(language%3Akuery%2Cquery%3A{{gquery}})%2Csort%3A!(_score%2Cdesc))\\\",\\\"description\\\":\\\"Machine OS win 7\\\",\\\"isDefault\\\":false,\\\"encoderID\\\":\\\"kql\\\",\\\"iconClass\\\":\\\"fa-share-alt\\\"}],\\\"exploreControls\\\":{\\\"useSignificance\\\":true,\\\"sampleSize\\\":2000,\\\"timeoutMillis\\\":5000,\\\"maxValuesPerDoc\\\":1,\\\"minDocCount\\\":3},\\\"indexPatternRefName\\\":\\\"indexPattern_0\\\"}\""},"coreMigrationVersion":"7.13.2","id":"6afc4b40-be5c-11eb-9520-1b4c3ca6a781","migrationVersion":{"graph-workspace":"7.11.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexPattern_0","type":"index-pattern"}],"sort":[1623693556928,634],"type":"graph-workspace","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2OTYsNF0="} -{"attributes":{"buildNum":39457,"defaultIndex":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0"},"coreMigrationVersion":"7.13.2","id":"7.12.1","migrationVersion":{"config":"7.13.0"},"references":[],"sort":[1623415891791,170],"type":"config","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0NTAsNF0="} -{"attributes":{"accessibility:disableAnimations":true,"buildNum":null,"dateFormat:tz":"UTC","defaultIndex":"56b34100-619d-11eb-aebf-c306684b328d","visualization:visualize:legacyChartsLibrary":true},"coreMigrationVersion":"7.13.2","id":"7.13.1","migrationVersion":{"config":"7.13.0"},"references":[],"sort":[1623693556928,635],"type":"config","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2OTcsNF0="} -{"attributes":{"buildNum":40943,"defaultIndex":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0"},"coreMigrationVersion":"7.13.2","id":"7.13.2","migrationVersion":{"config":"7.13.0"},"references":[],"sort":[1623693652730,748],"type":"config","updated_at":"2021-06-14T18:00:52.730Z","version":"WzE3MjQsNF0="} -{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"text_entry\",\"value\":\"Christendom.\",\"params\":{\"query\":\"Christendom.\",\"type\":\"phrase\"},\"disabled\":false,\"alias\":null,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"text_entry\":{\"query\":\"Christendom.\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["_score","desc"]],"title":"shakespeare_saved_search","version":1},"coreMigrationVersion":"7.13.2","id":"712ebbe0-619d-11eb-aebf-c306684b328d","migrationVersion":{"search":"7.9.3"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"}],"sort":[1623693556928,638],"type":"search","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2OTgsNF0="} -{"attributes":{"columns":["play_name","speaker"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"speaker:\\\"GLOUCESTER\\\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["_score","desc"]],"title":"shakespeare_saved_lucene_search","version":1},"coreMigrationVersion":"7.13.2","id":"ddacc820-619d-11eb-aebf-c306684b328d","migrationVersion":{"search":"7.9.3"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,640],"type":"search","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE2OTksNF0="} -{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"text_entry :\\\"MORDAKE THE EARL OF FIFE, AND ELDEST SON\\\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["_score","desc"]],"title":"shakespeare_saved_kql_search","version":1},"coreMigrationVersion":"7.13.2","id":"f852d570-619d-11eb-aebf-c306684b328d","migrationVersion":{"search":"7.9.3"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623693556928,642],"type":"search","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MDAsNF0="} -{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"3\",\"w\":24,\"x\":0,\"y\":15},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"h\":15,\"i\":\"4\",\"w\":24,\"x\":24,\"y\":15},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"h\":15,\"i\":\"5\",\"w\":24,\"x\":0,\"y\":30},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"h\":15,\"i\":\"6\",\"w\":24,\"x\":24,\"y\":30},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"}]","timeRestore":false,"title":"shakespeare_dashboard","version":1},"coreMigrationVersion":"7.13.2","id":"73398a90-619e-11eb-aebf-c306684b328d","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"185283c0-619e-11eb-aebf-c306684b328d","name":"1:panel_1","type":"visualization"},{"id":"33736660-619e-11eb-aebf-c306684b328d","name":"2:panel_2","type":"visualization"},{"id":"622ac7f0-619e-11eb-aebf-c306684b328d","name":"3:panel_3","type":"visualization"},{"id":"712ebbe0-619d-11eb-aebf-c306684b328d","name":"4:panel_4","type":"search"},{"id":"ddacc820-619d-11eb-aebf-c306684b328d","name":"5:panel_5","type":"search"},{"id":"f852d570-619d-11eb-aebf-c306684b328d","name":"6:panel_6","type":"search"}],"sort":[1623693556928,649],"type":"dashboard","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MDEsNF0="} -{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"geo.srcdest\",\"value\":\"IN:US\",\"params\":{\"query\":\"IN:US\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"geo.srcdest\":{\"query\":\"IN:US\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}]}"},"optionsJSON":"{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"3\",\"w\":24,\"x\":0,\"y\":15},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"4\",\"w\":24,\"x\":24,\"y\":15},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"5\",\"w\":24,\"x\":0,\"y\":30},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"6\",\"w\":24,\"x\":24,\"y\":30},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"7\",\"w\":24,\"x\":0,\"y\":45},\"panelIndex\":\"7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"8\",\"w\":24,\"x\":24,\"y\":45},\"panelIndex\":\"8\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"9\",\"w\":24,\"x\":0,\"y\":60},\"panelIndex\":\"9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"10\",\"w\":24,\"x\":24,\"y\":60},\"panelIndex\":\"10\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_10\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"11\",\"w\":24,\"x\":0,\"y\":75},\"panelIndex\":\"11\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_11\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"12\",\"w\":24,\"x\":24,\"y\":75},\"panelIndex\":\"12\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_12\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"13\",\"w\":24,\"x\":0,\"y\":90},\"panelIndex\":\"13\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"14\",\"w\":24,\"x\":24,\"y\":90},\"panelIndex\":\"14\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_14\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"15\",\"w\":24,\"x\":0,\"y\":105},\"panelIndex\":\"15\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_15\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"16\",\"w\":24,\"x\":24,\"y\":105},\"panelIndex\":\"16\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_16\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"17\",\"w\":24,\"x\":0,\"y\":120},\"panelIndex\":\"17\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_17\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"18\",\"w\":24,\"x\":24,\"y\":120},\"panelIndex\":\"18\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_18\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"19\",\"w\":24,\"x\":0,\"y\":135},\"panelIndex\":\"19\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_19\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"h\":15,\"i\":\"20\",\"w\":24,\"x\":24,\"y\":135},\"panelIndex\":\"20\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_20\"}]","timeRestore":false,"title":"logstash_dashboardwithfilters","version":1},"coreMigrationVersion":"7.13.2","id":"79794f20-6249-11eb-aebf-c306684b328d","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"},{"id":"36b91810-6239-11eb-aebf-c306684b328d","name":"1:panel_1","type":"visualization"},{"id":"0a274320-61cc-11eb-aebf-c306684b328d","name":"2:panel_2","type":"visualization"},{"id":"e4aef350-623d-11eb-aebf-c306684b328d","name":"3:panel_3","type":"visualization"},{"id":"f92e5630-623e-11eb-aebf-c306684b328d","name":"4:panel_4","type":"visualization"},{"id":"9853d4d0-623d-11eb-aebf-c306684b328d","name":"5:panel_5","type":"visualization"},{"id":"6ecb33b0-623d-11eb-aebf-c306684b328d","name":"6:panel_6","type":"visualization"},{"id":"b8e35c80-623c-11eb-aebf-c306684b328d","name":"7:panel_7","type":"visualization"},{"id":"f1bc75d0-6239-11eb-aebf-c306684b328d","name":"8:panel_8","type":"visualization"},{"id":"0d8a8860-623a-11eb-aebf-c306684b328d","name":"9:panel_9","type":"visualization"},{"id":"d79fe3d0-6239-11eb-aebf-c306684b328d","name":"10:panel_10","type":"visualization"},{"id":"318375a0-6240-11eb-aebf-c306684b328d","name":"11:panel_11","type":"visualization"},{"id":"e461eb20-6245-11eb-aebf-c306684b328d","name":"12:panel_12","type":"visualization"},{"id":"25bdc750-6242-11eb-aebf-c306684b328d","name":"13:panel_13","type":"visualization"},{"id":"71dd7bc0-6248-11eb-aebf-c306684b328d","name":"14:panel_14","type":"visualization"},{"id":"6aea48a0-6240-11eb-aebf-c306684b328d","name":"15:panel_15","type":"visualization"},{"id":"32b681f0-6241-11eb-aebf-c306684b328d","name":"16:panel_16","type":"visualization"},{"id":"ccca99e0-6244-11eb-aebf-c306684b328d","name":"17:panel_17","type":"visualization"},{"id":"a4d7be80-6245-11eb-aebf-c306684b328d","name":"18:panel_18","type":"visualization"},{"id":"c94d8440-6248-11eb-aebf-c306684b328d","name":"19:panel_19","type":"visualization"},{"id":"db6226f0-61c0-11eb-aebf-c306684b328d","name":"20:panel_20","type":"search"}],"sort":[1623693556928,671],"type":"dashboard","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MDIsNF0="} -{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"037b7937-790b-4d2d-94a5-7f5837a6ef05":{"columnOrder":["b3d46616-75e0-419e-97ea-91148961ef94","025a0fb3-dc44-4f5c-b517-2d71d3f26f14","c476db14-0cc1-40ec-863e-d2779256a407"],"columns":{"025a0fb3-dc44-4f5c-b517-2d71d3f26f14":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"b3d46616-75e0-419e-97ea-91148961ef94":{"dataType":"string","isBucketed":true,"label":"Top values of geo.srcdest","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"c476db14-0cc1-40ec-863e-d2779256a407","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"geo.srcdest"},"c476db14-0cc1-40ec-863e-d2779256a407":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"lucene","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["c476db14-0cc1-40ec-863e-d2779256a407"],"layerId":"037b7937-790b-4d2d-94a5-7f5837a6ef05","position":"top","seriesType":"bar_stacked","showGridlines":false,"splitAccessor":"b3d46616-75e0-419e-97ea-91148961ef94","xAccessor":"025a0fb3-dc44-4f5c-b517-2d71d3f26f14"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_verticalstacked","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.2","id":"8dc19b50-be32-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-037b7937-790b-4d2d-94a5-7f5837a6ef05","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,675],"type":"lens","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MDMsNF0="} -{"attributes":{"columns":[],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[{\"meta\":{\"alias\":null,\"negate\":false,\"disabled\":false,\"type\":\"phrase\",\"key\":\"geo.dest\",\"params\":{\"query\":\"US\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match_phrase\":{\"geo.dest\":\"US\"}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["@timestamp","desc"]],"title":"drilldown_logstash","version":1},"coreMigrationVersion":"7.13.2","id":"b3288100-ca2c-11eb-bf5e-3de94e83d4f0","migrationVersion":{"search":"7.9.3"},"references":[{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"}],"sort":[1623415891791,216],"type":"search","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0NjMsNF0="} -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{}"},"title":"logstash_timelion_panel","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_timelion_panel\",\"type\":\"timelion\",\"aggs\":[],\"params\":{\"expression\":\".es(index=logstash-*, \\\"sum:bytes\\\")\",\"interval\":\"auto\"}}"},"coreMigrationVersion":"7.13.2","id":"b3a44cd0-be5c-11eb-9520-1b4c3ca6a781","migrationVersion":{"visualization":"7.13.1"},"references":[],"sort":[1623693556928,677],"type":"visualization","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MDUsNF0="} -{"attributes":{"color":"#9170B8","description":"","name":"alltogether"},"coreMigrationVersion":"7.13.2","id":"be808cb0-be32-11eb-9520-1b4c3ca6a781","references":[],"sort":[1623693556928,678],"type":"tag","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MDYsNF0="} -{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"4d9e9a01-cdb8-4aef-afcb-50db52247bb1\"},\"panelIndex\":\"4d9e9a01-cdb8-4aef-afcb-50db52247bb1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4d9e9a01-cdb8-4aef-afcb-50db52247bb1\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"d9cab9c8-667e-4d34-821b-cbb070891956\"},\"panelIndex\":\"d9cab9c8-667e-4d34-821b-cbb070891956\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_d9cab9c8-667e-4d34-821b-cbb070891956\"}]","refreshInterval":{"pause":true,"value":0},"timeFrom":"2015-09-20T01:56:56.132Z","timeRestore":true,"timeTo":"2015-09-21T11:18:20.471Z","title":"lens_combined_dashboard","version":1},"coreMigrationVersion":"7.13.2","id":"bfb3dc90-be32-11eb-9520-1b4c3ca6a781","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"8dc19b50-be32-11eb-9520-1b4c3ca6a781","name":"4d9e9a01-cdb8-4aef-afcb-50db52247bb1:panel_4d9e9a01-cdb8-4aef-afcb-50db52247bb1","type":"lens"},{"id":"b5bd5050-be31-11eb-9520-1b4c3ca6a781","name":"d9cab9c8-667e-4d34-821b-cbb070891956:panel_d9cab9c8-667e-4d34-821b-cbb070891956","type":"lens"},{"id":"be808cb0-be32-11eb-9520-1b4c3ca6a781","name":"tag-be808cb0-be32-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,682],"type":"dashboard","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MDcsNF0="} -{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"2e80716f-c1b6-46f2-be2b-35db744b5031\"},\"panelIndex\":\"2e80716f-c1b6-46f2-be2b-35db744b5031\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsPie\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"26e2cf99-d931-4320-9e15-9dbc148f3534\":{\"columns\":{\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\":{\"label\":\"Top values of url.raw\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"url.raw\",\"isBucketed\":true,\"params\":{\"size\":20,\"orderBy\":{\"type\":\"column\",\"columnId\":\"beb72af1-239c-46d8-823b-b00d1e2ace43\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"beb72af1-239c-46d8-823b-b00d1e2ace43\":{\"label\":\"Unique count of geo.srcdest\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"geo.srcdest\",\"isBucketed\":false}},\"columnOrder\":[\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"beb72af1-239c-46d8-823b-b00d1e2ace43\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"shape\":\"donut\",\"layers\":[{\"layerId\":\"26e2cf99-d931-4320-9e15-9dbc148f3534\",\"groups\":[\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\"],\"metric\":\"beb72af1-239c-46d8-823b-b00d1e2ace43\",\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"default\",\"nestedLegend\":false}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-26e2cf99-d931-4320-9e15-9dbc148f3534\"}]},\"enhancements\":{},\"type\":\"lens\"},\"panelRefName\":\"panel_2e80716f-c1b6-46f2-be2b-35db744b5031\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"da8843e0-6789-4aae-bcd0-81f270538719\"},\"panelIndex\":\"da8843e0-6789-4aae-bcd0-81f270538719\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_da8843e0-6789-4aae-bcd0-81f270538719\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"adcd4418-7299-4efa-b369-5f71a7b4ebe0\"},\"panelIndex\":\"adcd4418-7299-4efa-b369-5f71a7b4ebe0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_adcd4418-7299-4efa-b369-5f71a7b4ebe0\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"869754a7-edf0-478f-a7f1-80374f63108a\"},\"panelIndex\":\"869754a7-edf0-478f-a7f1-80374f63108a\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_869754a7-edf0-478f-a7f1-80374f63108a\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":30,\"w\":24,\"h\":15,\"i\":\"67111cf4-338e-453f-8621-e8dea64082d1\"},\"panelIndex\":\"67111cf4-338e-453f-8621-e8dea64082d1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_67111cf4-338e-453f-8621-e8dea64082d1\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\"},\"panelIndex\":\"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":45,\"w\":24,\"h\":15,\"i\":\"88847944-ae1b-45fd-b102-3b45f9bea04b\"},\"panelIndex\":\"88847944-ae1b-45fd-b102-3b45f9bea04b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_88847944-ae1b-45fd-b102-3b45f9bea04b\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":45,\"w\":24,\"h\":15,\"i\":\"5a7924c7-eac0-4573-9199-fecec5b82e9e\"},\"panelIndex\":\"5a7924c7-eac0-4573-9199-fecec5b82e9e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5a7924c7-eac0-4573-9199-fecec5b82e9e\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":60,\"w\":24,\"h\":15,\"i\":\"f8f49591-f071-4a96-b1ed-cd65daff5648\"},\"panelIndex\":\"f8f49591-f071-4a96-b1ed-cd65daff5648\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_f8f49591-f071-4a96-b1ed-cd65daff5648\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":60,\"w\":24,\"h\":15,\"i\":\"9f357f47-c2a0-421f-a456-9583c40837ab\"},\"panelIndex\":\"9f357f47-c2a0-421f-a456-9583c40837ab\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9f357f47-c2a0-421f-a456-9583c40837ab\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":75,\"w\":24,\"h\":15,\"i\":\"6cb383e9-1e80-44f9-80d5-7b8c585668db\"},\"panelIndex\":\"6cb383e9-1e80-44f9-80d5-7b8c585668db\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6cb383e9-1e80-44f9-80d5-7b8c585668db\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":75,\"w\":24,\"h\":15,\"i\":\"57f5f0bf-6610-4599-aad4-37484640b5e2\"},\"panelIndex\":\"57f5f0bf-6610-4599-aad4-37484640b5e2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_57f5f0bf-6610-4599-aad4-37484640b5e2\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":90,\"w\":24,\"h\":15,\"i\":\"32d3ab66-52e1-44e3-8c1f-1dccff3c5692\"},\"panelIndex\":\"32d3ab66-52e1-44e3-8c1f-1dccff3c5692\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_32d3ab66-52e1-44e3-8c1f-1dccff3c5692\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":90,\"w\":24,\"h\":15,\"i\":\"dd1718fd-74ee-4032-851b-db97e893825d\"},\"panelIndex\":\"dd1718fd-74ee-4032-851b-db97e893825d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_dd1718fd-74ee-4032-851b-db97e893825d\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":105,\"w\":24,\"h\":15,\"i\":\"98a556ee-078b-4e03-93a8-29996133cdcb\"},\"panelIndex\":\"98a556ee-078b-4e03-93a8-29996133cdcb\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"999a2d60-cb2a-451c-8d71-80d7e92e70fd\":{\"columns\":{\"ce9117a2-773c-474c-8fb1-18940cf58b38\":{\"label\":\"Top values of type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"type\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\"},\"orderDirection\":\"asc\",\"otherBucket\":true,\"missingBucket\":false}},\"a3d10552-e352-40d0-a156-e86112c0501a\":{\"label\":\"Top values of _type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"_type\",\"isBucketed\":true,\"params\":{\"size\":3,\"orderBy\":{\"type\":\"column\",\"columnId\":\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"},\"9c5db2f3-9eb0-4667-9a74-3318301de251\":{\"label\":\"Sum of bytes\",\"dataType\":\"number\",\"operationType\":\"sum\",\"sourceField\":\"bytes\",\"isBucketed\":false,\"scale\":\"ratio\"}},\"columnOrder\":[\"ce9117a2-773c-474c-8fb1-18940cf58b38\",\"a3d10552-e352-40d0-a156-e86112c0501a\",\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\",\"9c5db2f3-9eb0-4667-9a74-3318301de251\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"999a2d60-cb2a-451c-8d71-80d7e92e70fd\",\"accessors\":[\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\",\"9c5db2f3-9eb0-4667-9a74-3318301de251\"],\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"xAccessor\":\"ce9117a2-773c-474c-8fb1-18940cf58b38\",\"splitAccessor\":\"a3d10552-e352-40d0-a156-e86112c0501a\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-999a2d60-cb2a-451c-8d71-80d7e92e70fd\"}]},\"enhancements\":{},\"type\":\"lens\"}},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":105,\"w\":24,\"h\":15,\"i\":\"62a0f0b0-3589-4cef-807b-b1b4258b7a9b\"},\"panelIndex\":\"62a0f0b0-3589-4cef-807b-b1b4258b7a9b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_62a0f0b0-3589-4cef-807b-b1b4258b7a9b\"},{\"version\":\"7.13.1\",\"type\":\"map\",\"gridData\":{\"x\":0,\"y\":120,\"w\":24,\"h\":15,\"i\":\"dcc0defa-3376-465c-9b5b-2ba69528848c\"},\"panelIndex\":\"dcc0defa-3376-465c-9b5b-2ba69528848c\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":19.94277,\"lon\":0,\"zoom\":1.56},\"mapBuffer\":{\"minLon\":-210.32666,\"minLat\":-64.8435,\"maxLon\":210.32666,\"maxLat\":95.13806},\"isLayerTOCOpen\":true,\"openTOCDetails\":[],\"hiddenLayers\":[],\"enhancements\":{}},\"panelRefName\":\"panel_dcc0defa-3376-465c-9b5b-2ba69528848c\"},{\"version\":\"7.13.1\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":120,\"w\":24,\"h\":15,\"i\":\"dd21a674-ae3a-40f6-9d68-4e01361ea5e2\"},\"panelIndex\":\"dd21a674-ae3a-40f6-9d68-4e01361ea5e2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_dd21a674-ae3a-40f6-9d68-4e01361ea5e2\"}]","refreshInterval":{"pause":true,"value":0},"timeFrom":"2015-09-20T01:56:56.132Z","timeRestore":true,"timeTo":"2015-09-21T11:18:20.471Z","title":"timelion_lens_maps_dashboard_logstash","version":1},"coreMigrationVersion":"7.13.2","id":"c4ab2030-be5c-11eb-9520-1b4c3ca6a781","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"21905950-bd9f-11eb-9520-1b4c3ca6a781","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:panel_2e80716f-c1b6-46f2-be2b-35db744b5031","type":"lens"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:indexpattern-datasource-layer-26e2cf99-d931-4320-9e15-9dbc148f3534","type":"index-pattern"},{"id":"aa4b8da0-bd9f-11eb-9520-1b4c3ca6a781","name":"da8843e0-6789-4aae-bcd0-81f270538719:panel_da8843e0-6789-4aae-bcd0-81f270538719","type":"lens"},{"id":"2d3f1250-bd9f-11eb-9520-1b4c3ca6a781","name":"adcd4418-7299-4efa-b369-5f71a7b4ebe0:panel_adcd4418-7299-4efa-b369-5f71a7b4ebe0","type":"lens"},{"id":"edd5a560-bda4-11eb-9520-1b4c3ca6a781","name":"869754a7-edf0-478f-a7f1-80374f63108a:panel_869754a7-edf0-478f-a7f1-80374f63108a","type":"lens"},{"id":"2c25a450-bda5-11eb-9520-1b4c3ca6a781","name":"67111cf4-338e-453f-8621-e8dea64082d1:panel_67111cf4-338e-453f-8621-e8dea64082d1","type":"lens"},{"id":"e79116e0-bd9e-11eb-9520-1b4c3ca6a781","name":"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d:panel_13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d","type":"lens"},{"id":"974fb950-bda5-11eb-9520-1b4c3ca6a781","name":"88847944-ae1b-45fd-b102-3b45f9bea04b:panel_88847944-ae1b-45fd-b102-3b45f9bea04b","type":"lens"},{"id":"21905950-bd9f-11eb-9520-1b4c3ca6a781","name":"5a7924c7-eac0-4573-9199-fecec5b82e9e:panel_5a7924c7-eac0-4573-9199-fecec5b82e9e","type":"lens"},{"id":"51b63040-bda5-11eb-9520-1b4c3ca6a781","name":"f8f49591-f071-4a96-b1ed-cd65daff5648:panel_f8f49591-f071-4a96-b1ed-cd65daff5648","type":"lens"},{"id":"b00679c0-bda5-11eb-9520-1b4c3ca6a781","name":"9f357f47-c2a0-421f-a456-9583c40837ab:panel_9f357f47-c2a0-421f-a456-9583c40837ab","type":"lens"},{"id":"652ade10-bd9f-11eb-9520-1b4c3ca6a781","name":"6cb383e9-1e80-44f9-80d5-7b8c585668db:panel_6cb383e9-1e80-44f9-80d5-7b8c585668db","type":"lens"},{"id":"7f3b5fb0-be2f-11eb-9520-1b4c3ca6a781","name":"57f5f0bf-6610-4599-aad4-37484640b5e2:panel_57f5f0bf-6610-4599-aad4-37484640b5e2","type":"lens"},{"id":"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781","name":"32d3ab66-52e1-44e3-8c1f-1dccff3c5692:panel_32d3ab66-52e1-44e3-8c1f-1dccff3c5692","type":"lens"},{"id":"dd315430-be2f-11eb-9520-1b4c3ca6a781","name":"dd1718fd-74ee-4032-851b-db97e893825d:panel_dd1718fd-74ee-4032-851b-db97e893825d","type":"lens"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"98a556ee-078b-4e03-93a8-29996133cdcb:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"98a556ee-078b-4e03-93a8-29996133cdcb:indexpattern-datasource-layer-999a2d60-cb2a-451c-8d71-80d7e92e70fd","type":"index-pattern"},{"id":"0dbbf8b0-be3c-11eb-9520-1b4c3ca6a781","name":"62a0f0b0-3589-4cef-807b-b1b4258b7a9b:panel_62a0f0b0-3589-4cef-807b-b1b4258b7a9b","type":"lens"},{"id":"0c5974f0-be5c-11eb-9520-1b4c3ca6a781","name":"dcc0defa-3376-465c-9b5b-2ba69528848c:panel_dcc0defa-3376-465c-9b5b-2ba69528848c","type":"map"},{"id":"a4d7be80-6245-11eb-aebf-c306684b328d","name":"dd21a674-ae3a-40f6-9d68-4e01361ea5e2:panel_dd21a674-ae3a-40f6-9d68-4e01361ea5e2","type":"visualization"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,705],"type":"dashboard","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MDgsNF0="} -{"attributes":{"columns":[],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"speaker :\\\"DUKE VINCENTIO\\\" \",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[],"title":"drilldown_shakes","version":1},"coreMigrationVersion":"7.13.2","id":"c4b9cc00-ca2a-11eb-bf5e-3de94e83d4f0","migrationVersion":{"search":"7.9.3"},"references":[{"id":"39d52f60-ca27-11eb-bf5e-3de94e83d4f0","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1623415891791,218],"type":"search","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0NjQsNF0="} -{"attributes":{"@created":"2021-05-27T19:45:29.712Z","@timestamp":"2021-05-27T19:45:29.712Z","content":"{\"selectedNodes\":[{\"id\":\"element-56d2ba72-f227-4d04-9478-a1d6f0c7e601\",\"position\":{\"left\":20,\"top\":20,\"width\":500,\"height\":300,\"angle\":0,\"parent\":\"group-499b5982-25f4-4894-9540-1874a27d78e7\",\"type\":\"element\"},\"expression\":\"savedLens id=\\\"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781\\\" timerange={timerange from=\\\"now-15y\\\" to=\\\"now\\\"}\\n| render\",\"filter\":null,\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"savedLens\",\"arguments\":{\"id\":[\"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781\"],\"timerange\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"timerange\",\"arguments\":{\"from\":[\"now-15y\"],\"to\":[\"now\"]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-afbaa26e-10e7-47d4-bb41-b061dfdced2b\",\"position\":{\"left\":527,\"top\":20,\"width\":500,\"height\":300,\"angle\":0,\"parent\":\"group-499b5982-25f4-4894-9540-1874a27d78e7\",\"type\":\"element\"},\"expression\":\"savedVisualization id=\\\"0d8a8860-623a-11eb-aebf-c306684b328d\\\" timerange={timerange from=\\\"now-15y\\\" to=\\\"now\\\"}\\n| render\",\"filter\":null,\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"savedVisualization\",\"arguments\":{\"id\":[\"0d8a8860-623a-11eb-aebf-c306684b328d\"],\"timerange\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"timerange\",\"arguments\":{\"from\":[\"now-15y\"],\"to\":[\"now\"]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}","displayName":"element_canvas","help":"","image":"","name":"elementCanvas"},"coreMigrationVersion":"7.13.2","id":"custom-element-3bc52277-ee01-4cdc-8d2d-f2db6ade1512","references":[],"sort":[1623693556928,706],"type":"canvas-element","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MDksNF0="} -{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"ced0a5ea-3ec2-4274-8431-6e76d85637f6\"},\"panelIndex\":\"ced0a5ea-3ec2-4274-8431-6e76d85637f6\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"6a3b206a-4ac3-4d67-b2b9-c6b543a11ea3\":{\"columns\":{\"f70668f8-ae97-4b64-867f-b0c9b77914ef\":{\"label\":\"Top values of speaker\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"speaker\",\"isBucketed\":true,\"params\":{\"size\":39,\"orderBy\":{\"type\":\"column\",\"columnId\":\"fbf256d9-cae7-4244-8504-b73a5666e917\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"fbf256d9-cae7-4244-8504-b73a5666e917\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"}},\"columnOrder\":[\"f70668f8-ae97-4b64-867f-b0c9b77914ef\",\"fbf256d9-cae7-4244-8504-b73a5666e917\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_horizontal_percentage_stacked\",\"layers\":[{\"layerId\":\"6a3b206a-4ac3-4d67-b2b9-c6b543a11ea3\",\"accessors\":[\"fbf256d9-cae7-4244-8504-b73a5666e917\"],\"position\":\"top\",\"seriesType\":\"bar_horizontal_percentage_stacked\",\"showGridlines\":false,\"splitAccessor\":\"f70668f8-ae97-4b64-867f-b0c9b77914ef\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"39d52f60-ca27-11eb-bf5e-3de94e83d4f0\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"39d52f60-ca27-11eb-bf5e-3de94e83d4f0\",\"name\":\"indexpattern-datasource-layer-6a3b206a-4ac3-4d67-b2b9-c6b543a11ea3\"}]},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"2b9a2bad-d6aa-4d3b-a692-fd96c3fb0ac1\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"We_like_lens\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}},\"type\":\"lens\"}},{\"version\":\"7.13.1\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"5e4cf03a-13bb-4aa7-8326-b47a19e88968\"},\"panelIndex\":\"5e4cf03a-13bb-4aa7-8326-b47a19e88968\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"play_name\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":788,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"39d52f60-ca27-11eb-bf5e-3de94e83d4f0\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"c5e2a416-2985-4f44-a6b6-70bb95d3bcdd\",\"triggers\":[\"CONTEXT_MENU_TRIGGER\"],\"action\":{\"name\":\"shakespeare_discover\",\"config\":{\"url\":{\"template\":\"http://localhost:5601/app/discover#/view/c4b9cc00-ca2a-11eb-bf5e-3de94e83d4f0?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&_a=(columns:!(),filters:!(),index:'39d52f60-ca27-11eb-bf5e-3de94e83d4f0',interval:auto,query:(language:kuery,query:'speaker%20:%22DUKE%20VINCENTIO%22%20'),sort:!())\"},\"openInNewTab\":true,\"encodeUrl\":true},\"factoryId\":\"URL_DRILLDOWN\"}},{\"eventId\":\"de71a757-6401-4b05-9d8d-475fedc0cd47\",\"triggers\":[\"VALUE_CLICK_TRIGGER\"],\"action\":{\"name\":\"drilldown_timebased\",\"config\":{\"url\":{\"template\":\"http://localhost:5601/app/discover#/view/b3288100-ca2c-11eb-bf5e-3de94e83d4f0?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&_a=(columns:!(),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'43fcac20-ca27-11eb-bf5e-3de94e83d4f0',key:geo.dest,negate:!f,params:(query:US),type:phrase),query:(match_phrase:(geo.dest:US)))),index:'43fcac20-ca27-11eb-bf5e-3de94e83d4f0',interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))\"},\"openInNewTab\":true,\"encodeUrl\":true},\"factoryId\":\"URL_DRILLDOWN\"}}]}},\"type\":\"visualization\"}}]","timeRestore":false,"title":"nontimebased_shakespeare_drilldown","version":1},"coreMigrationVersion":"7.13.2","id":"e9eb20f0-ca2a-11eb-bf5e-3de94e83d4f0","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"39d52f60-ca27-11eb-bf5e-3de94e83d4f0","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"39d52f60-ca27-11eb-bf5e-3de94e83d4f0","name":"indexpattern-datasource-layer-6a3b206a-4ac3-4d67-b2b9-c6b543a11ea3","type":"index-pattern"},{"id":"08dec860-ca29-11eb-bf5e-3de94e83d4f0","name":"drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:2b9a2bad-d6aa-4d3b-a692-fd96c3fb0ac1:dashboardId","type":"dashboard"},{"id":"39d52f60-ca27-11eb-bf5e-3de94e83d4f0","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"07f48f70-ca29-11eb-bf5e-3de94e83d4f0","name":"tag-07f48f70-ca29-11eb-bf5e-3de94e83d4f0","type":"tag"}],"sort":[1623415891791,224],"type":"dashboard","updated_at":"2021-06-11T12:51:31.791Z","version":"WzE0NjUsNF0="} -{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"2e80716f-c1b6-46f2-be2b-35db744b5031\"},\"panelIndex\":\"2e80716f-c1b6-46f2-be2b-35db744b5031\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsPie\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"26e2cf99-d931-4320-9e15-9dbc148f3534\":{\"columns\":{\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\":{\"label\":\"Top values of url.raw\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"url.raw\",\"isBucketed\":true,\"params\":{\"size\":20,\"orderBy\":{\"type\":\"column\",\"columnId\":\"beb72af1-239c-46d8-823b-b00d1e2ace43\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"beb72af1-239c-46d8-823b-b00d1e2ace43\":{\"label\":\"Unique count of geo.srcdest\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"geo.srcdest\",\"isBucketed\":false}},\"columnOrder\":[\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"beb72af1-239c-46d8-823b-b00d1e2ace43\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"shape\":\"donut\",\"layers\":[{\"layerId\":\"26e2cf99-d931-4320-9e15-9dbc148f3534\",\"groups\":[\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\"],\"metric\":\"beb72af1-239c-46d8-823b-b00d1e2ace43\",\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"default\",\"nestedLegend\":false}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-26e2cf99-d931-4320-9e15-9dbc148f3534\"}]},\"enhancements\":{},\"type\":\"lens\"},\"panelRefName\":\"panel_2e80716f-c1b6-46f2-be2b-35db744b5031\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"da8843e0-6789-4aae-bcd0-81f270538719\"},\"panelIndex\":\"da8843e0-6789-4aae-bcd0-81f270538719\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_da8843e0-6789-4aae-bcd0-81f270538719\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"adcd4418-7299-4efa-b369-5f71a7b4ebe0\"},\"panelIndex\":\"adcd4418-7299-4efa-b369-5f71a7b4ebe0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_adcd4418-7299-4efa-b369-5f71a7b4ebe0\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"869754a7-edf0-478f-a7f1-80374f63108a\"},\"panelIndex\":\"869754a7-edf0-478f-a7f1-80374f63108a\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_869754a7-edf0-478f-a7f1-80374f63108a\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":30,\"w\":24,\"h\":15,\"i\":\"67111cf4-338e-453f-8621-e8dea64082d1\"},\"panelIndex\":\"67111cf4-338e-453f-8621-e8dea64082d1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_67111cf4-338e-453f-8621-e8dea64082d1\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\"},\"panelIndex\":\"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":45,\"w\":24,\"h\":15,\"i\":\"88847944-ae1b-45fd-b102-3b45f9bea04b\"},\"panelIndex\":\"88847944-ae1b-45fd-b102-3b45f9bea04b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_88847944-ae1b-45fd-b102-3b45f9bea04b\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":45,\"w\":24,\"h\":15,\"i\":\"5a7924c7-eac0-4573-9199-fecec5b82e9e\"},\"panelIndex\":\"5a7924c7-eac0-4573-9199-fecec5b82e9e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5a7924c7-eac0-4573-9199-fecec5b82e9e\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":60,\"w\":24,\"h\":15,\"i\":\"f8f49591-f071-4a96-b1ed-cd65daff5648\"},\"panelIndex\":\"f8f49591-f071-4a96-b1ed-cd65daff5648\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_f8f49591-f071-4a96-b1ed-cd65daff5648\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":60,\"w\":24,\"h\":15,\"i\":\"9f357f47-c2a0-421f-a456-9583c40837ab\"},\"panelIndex\":\"9f357f47-c2a0-421f-a456-9583c40837ab\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9f357f47-c2a0-421f-a456-9583c40837ab\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":75,\"w\":24,\"h\":15,\"i\":\"6cb383e9-1e80-44f9-80d5-7b8c585668db\"},\"panelIndex\":\"6cb383e9-1e80-44f9-80d5-7b8c585668db\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6cb383e9-1e80-44f9-80d5-7b8c585668db\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":75,\"w\":24,\"h\":15,\"i\":\"57f5f0bf-6610-4599-aad4-37484640b5e2\"},\"panelIndex\":\"57f5f0bf-6610-4599-aad4-37484640b5e2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_57f5f0bf-6610-4599-aad4-37484640b5e2\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":90,\"w\":24,\"h\":15,\"i\":\"32d3ab66-52e1-44e3-8c1f-1dccff3c5692\"},\"panelIndex\":\"32d3ab66-52e1-44e3-8c1f-1dccff3c5692\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_32d3ab66-52e1-44e3-8c1f-1dccff3c5692\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":90,\"w\":24,\"h\":15,\"i\":\"dd1718fd-74ee-4032-851b-db97e893825d\"},\"panelIndex\":\"dd1718fd-74ee-4032-851b-db97e893825d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_dd1718fd-74ee-4032-851b-db97e893825d\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":105,\"w\":24,\"h\":15,\"i\":\"98a556ee-078b-4e03-93a8-29996133cdcb\"},\"panelIndex\":\"98a556ee-078b-4e03-93a8-29996133cdcb\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"999a2d60-cb2a-451c-8d71-80d7e92e70fd\":{\"columns\":{\"ce9117a2-773c-474c-8fb1-18940cf58b38\":{\"label\":\"Top values of type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"type\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\"},\"orderDirection\":\"asc\",\"otherBucket\":true,\"missingBucket\":false}},\"a3d10552-e352-40d0-a156-e86112c0501a\":{\"label\":\"Top values of _type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"_type\",\"isBucketed\":true,\"params\":{\"size\":3,\"orderBy\":{\"type\":\"column\",\"columnId\":\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"},\"9c5db2f3-9eb0-4667-9a74-3318301de251\":{\"label\":\"Sum of bytes\",\"dataType\":\"number\",\"operationType\":\"sum\",\"sourceField\":\"bytes\",\"isBucketed\":false,\"scale\":\"ratio\"}},\"columnOrder\":[\"ce9117a2-773c-474c-8fb1-18940cf58b38\",\"a3d10552-e352-40d0-a156-e86112c0501a\",\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\",\"9c5db2f3-9eb0-4667-9a74-3318301de251\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"999a2d60-cb2a-451c-8d71-80d7e92e70fd\",\"accessors\":[\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\",\"9c5db2f3-9eb0-4667-9a74-3318301de251\"],\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"xAccessor\":\"ce9117a2-773c-474c-8fb1-18940cf58b38\",\"splitAccessor\":\"a3d10552-e352-40d0-a156-e86112c0501a\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-999a2d60-cb2a-451c-8d71-80d7e92e70fd\"}]},\"enhancements\":{},\"type\":\"lens\"}},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":105,\"w\":24,\"h\":15,\"i\":\"62a0f0b0-3589-4cef-807b-b1b4258b7a9b\"},\"panelIndex\":\"62a0f0b0-3589-4cef-807b-b1b4258b7a9b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_62a0f0b0-3589-4cef-807b-b1b4258b7a9b\"}]","refreshInterval":{"pause":true,"value":0},"timeFrom":"2015-09-20T01:56:56.132Z","timeRestore":true,"timeTo":"2015-09-21T11:18:20.471Z","title":"lens_dashboard_logstash","version":1},"coreMigrationVersion":"7.13.2","id":"f458b9f0-bd9e-11eb-9520-1b4c3ca6a781","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"21905950-bd9f-11eb-9520-1b4c3ca6a781","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:panel_2e80716f-c1b6-46f2-be2b-35db744b5031","type":"lens"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:indexpattern-datasource-layer-26e2cf99-d931-4320-9e15-9dbc148f3534","type":"index-pattern"},{"id":"aa4b8da0-bd9f-11eb-9520-1b4c3ca6a781","name":"da8843e0-6789-4aae-bcd0-81f270538719:panel_da8843e0-6789-4aae-bcd0-81f270538719","type":"lens"},{"id":"2d3f1250-bd9f-11eb-9520-1b4c3ca6a781","name":"adcd4418-7299-4efa-b369-5f71a7b4ebe0:panel_adcd4418-7299-4efa-b369-5f71a7b4ebe0","type":"lens"},{"id":"edd5a560-bda4-11eb-9520-1b4c3ca6a781","name":"869754a7-edf0-478f-a7f1-80374f63108a:panel_869754a7-edf0-478f-a7f1-80374f63108a","type":"lens"},{"id":"2c25a450-bda5-11eb-9520-1b4c3ca6a781","name":"67111cf4-338e-453f-8621-e8dea64082d1:panel_67111cf4-338e-453f-8621-e8dea64082d1","type":"lens"},{"id":"e79116e0-bd9e-11eb-9520-1b4c3ca6a781","name":"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d:panel_13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d","type":"lens"},{"id":"974fb950-bda5-11eb-9520-1b4c3ca6a781","name":"88847944-ae1b-45fd-b102-3b45f9bea04b:panel_88847944-ae1b-45fd-b102-3b45f9bea04b","type":"lens"},{"id":"21905950-bd9f-11eb-9520-1b4c3ca6a781","name":"5a7924c7-eac0-4573-9199-fecec5b82e9e:panel_5a7924c7-eac0-4573-9199-fecec5b82e9e","type":"lens"},{"id":"51b63040-bda5-11eb-9520-1b4c3ca6a781","name":"f8f49591-f071-4a96-b1ed-cd65daff5648:panel_f8f49591-f071-4a96-b1ed-cd65daff5648","type":"lens"},{"id":"b00679c0-bda5-11eb-9520-1b4c3ca6a781","name":"9f357f47-c2a0-421f-a456-9583c40837ab:panel_9f357f47-c2a0-421f-a456-9583c40837ab","type":"lens"},{"id":"652ade10-bd9f-11eb-9520-1b4c3ca6a781","name":"6cb383e9-1e80-44f9-80d5-7b8c585668db:panel_6cb383e9-1e80-44f9-80d5-7b8c585668db","type":"lens"},{"id":"7f3b5fb0-be2f-11eb-9520-1b4c3ca6a781","name":"57f5f0bf-6610-4599-aad4-37484640b5e2:panel_57f5f0bf-6610-4599-aad4-37484640b5e2","type":"lens"},{"id":"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781","name":"32d3ab66-52e1-44e3-8c1f-1dccff3c5692:panel_32d3ab66-52e1-44e3-8c1f-1dccff3c5692","type":"lens"},{"id":"dd315430-be2f-11eb-9520-1b4c3ca6a781","name":"dd1718fd-74ee-4032-851b-db97e893825d:panel_dd1718fd-74ee-4032-851b-db97e893825d","type":"lens"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"98a556ee-078b-4e03-93a8-29996133cdcb:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"98a556ee-078b-4e03-93a8-29996133cdcb:indexpattern-datasource-layer-999a2d60-cb2a-451c-8d71-80d7e92e70fd","type":"index-pattern"},{"id":"0dbbf8b0-be3c-11eb-9520-1b4c3ca6a781","name":"62a0f0b0-3589-4cef-807b-b1b4258b7a9b:panel_62a0f0b0-3589-4cef-807b-b1b4258b7a9b","type":"lens"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1623693556928,727],"type":"dashboard","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MTAsNF0="} -{"attributes":{"allowNoIndex":true,"fieldFormatMap":"{\"Target.process.parent.pgid\":{\"id\":\"string\"},\"Target.process.parent.pid\":{\"id\":\"string\"},\"Target.process.parent.ppid\":{\"id\":\"string\"},\"Target.process.parent.thread.id\":{\"id\":\"string\"},\"Target.process.pgid\":{\"id\":\"string\"},\"Target.process.pid\":{\"id\":\"string\"},\"Target.process.ppid\":{\"id\":\"string\"},\"Target.process.thread.id\":{\"id\":\"string\"},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"process.parent.pgid\":{\"id\":\"string\"},\"process.parent.pid\":{\"id\":\"string\"},\"process.parent.ppid\":{\"id\":\"string\"},\"process.parent.thread.id\":{\"id\":\"string\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.port\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.port\":{\"id\":\"string\"}}","fields":"[{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"Endpoint.policy.applied.artifacts.global\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.global.identifiers\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.global.identifiers.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.global.identifiers.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.global.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.user\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.user.identifiers\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.user.identifiers.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.user.identifiers.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.user.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Memory_protection.cross_session\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Memory_protection.feature\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Memory_protection.parent_to_child\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Memory_protection.self_injection\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Memory_protection.thread_count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Memory_protection.unique_key_v1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Ransomware.child_pids\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Ransomware.feature\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Ransomware.files\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Ransomware.files.data\",\"type\":\"binary\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"Ransomware.files.entropy\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Ransomware.files.extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Ransomware.files.metrics\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Ransomware.files.operation\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Ransomware.files.original.extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Ransomware.files.original.path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Ransomware.files.path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Ransomware.files.score\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Ransomware.score\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Ransomware.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.code_signature\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.code_signature.exists\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.code_signature.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.code_signature.subject_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.code_signature.trusted\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.code_signature.valid\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.compile_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.malware_classification.features\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"Target.dll.Ext.malware_classification.features.data.buffer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.malware_classification.features.data.decompressed_size\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.malware_classification.features.data.encoding\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.malware_classification.identifier\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.malware_classification.score\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.malware_classification.threshold\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.malware_classification.upx_packed\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.malware_classification.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.mapped_address\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.Ext.mapped_size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.hash.md5\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.hash.sha1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.hash.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.hash.sha512\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.dll.pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.ancestry\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.architecture\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.authentication_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.code_signature\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.code_signature.exists\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.code_signature.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.code_signature.subject_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.code_signature.trusted\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.code_signature.valid\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.Ext.code_signature\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.Ext.code_signature.exists\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.Ext.code_signature.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.Ext.code_signature.subject_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.Ext.code_signature.trusted\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.Ext.code_signature.valid\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.Ext.compile_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.Ext.mapped_address\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.Ext.mapped_size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.hash.md5\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.hash.sha1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.hash.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.hash.sha512\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.dll.pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.malware_classification.features\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"Target.process.Ext.malware_classification.features.data.buffer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.malware_classification.features.data.decompressed_size\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.malware_classification.features.data.encoding\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.malware_classification.identifier\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.malware_classification.score\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.malware_classification.threshold\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.malware_classification.upx_packed\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.malware_classification.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.services\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.session\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.token.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.token.elevation\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.token.elevation_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.token.impersonation_level\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.token.integrity_level\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.token.integrity_level_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.token.is_appcontainer\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.token.privileges\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.token.privileges.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.token.privileges.enabled\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.token.privileges.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.token.sid\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.token.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.token.user\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.Ext.user\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.args\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.args_count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.command_line\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.command_line.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.command_line.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"Target.process.entity_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.executable\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.executable.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.executable.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"Target.process.exit_code\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.hash.md5\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.hash.sha1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.hash.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.hash.sha512\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.name.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.name.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.architecture\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.code_signature\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.code_signature.exists\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.code_signature.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.code_signature.subject_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.code_signature.trusted\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.code_signature.valid\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.Ext.code_signature\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.Ext.code_signature.exists\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.Ext.code_signature.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.Ext.code_signature.subject_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.Ext.code_signature.trusted\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.Ext.code_signature.valid\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.Ext.compile_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.Ext.mapped_address\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.Ext.mapped_size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.hash.md5\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.hash.sha1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.hash.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.hash.sha512\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.dll.pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.real\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.real.pid\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.token.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.token.elevation\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.token.elevation_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.token.impersonation_level\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.token.integrity_level\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.token.integrity_level_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.token.is_appcontainer\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.token.privileges\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.token.privileges.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.token.privileges.enabled\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.token.privileges.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.token.sid\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.token.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.token.user\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.Ext.user\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.args\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.args_count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.command_line\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.command_line.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.command_line.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.entity_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.executable\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.executable.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.executable.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.exit_code\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.hash.md5\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.hash.sha1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.hash.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.hash.sha512\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.name.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.name.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.pgid\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.pid\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.ppid\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.start\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.thread.id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.thread.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.title.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.uptime\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.working_directory\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.working_directory.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.parent.working_directory.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"Target.process.pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.pgid\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.pid\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.ppid\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.start\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.call_stack.instruction_pointer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.call_stack.memory_section.address\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.call_stack.memory_section.protection\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.call_stack.memory_section.size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.call_stack.module_path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.call_stack.rva\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.call_stack.symbol_info\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.parameter\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.parameter_bytes_compressed\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.parameter_bytes_compressed_present\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.service\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_allocation_offset\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_bytes\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_bytes_disasm\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_bytes_disasm_hash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.allocation_base\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.allocation_protection\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.allocation_size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.allocation_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.bytes_address\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.bytes_allocation_offset\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.bytes_compressed\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.bytes_compressed_present\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.mapped_pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.mapped_pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.mapped_pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.mapped_pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.mapped_pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.mapped_pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.mapped_pe_detected\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.memory_pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.memory_pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.memory_pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.memory_pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.memory_pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.memory_pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.memory_pe_detected\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.region_base\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.region_protection\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.region_size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.region_state\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_details.strings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.start_address_module\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.token.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.token.elevation\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.token.elevation_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.token.impersonation_level\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.token.integrity_level\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.token.integrity_level_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.token.is_appcontainer\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.token.privileges\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.token.privileges.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.token.privileges.enabled\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.token.privileges.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.token.sid\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.token.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.token.user\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.Ext.uptime\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.thread.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.title.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"Target.process.uptime\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.working_directory\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.working_directory.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Target.process.working_directory.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"agent.ephemeral_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"data_stream.dataset\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"data_stream.namespace\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"data_stream.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.city_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.continent_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.country_iso_code\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.country_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.location\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.region_iso_code\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.geo.region_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.code_signature\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.code_signature.exists\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.code_signature.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.code_signature.subject_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.code_signature.trusted\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.code_signature.valid\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.compile_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.malware_classification.features\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"dll.Ext.malware_classification.features.data.buffer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.malware_classification.features.data.decompressed_size\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.malware_classification.features.data.encoding\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.malware_classification.identifier\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.malware_classification.score\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.malware_classification.threshold\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.malware_classification.upx_packed\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.malware_classification.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.mapped_address\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.mapped_size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.hash.md5\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.hash.sha1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.hash.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.hash.sha512\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ecs.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"elastic.agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"elastic.agent.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.action\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.category\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.code\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.created\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.dataset\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.hash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.ingested\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.kind\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.module\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.outcome\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.provider\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.sequence\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.severity\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.code_signature\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.code_signature.exists\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.code_signature.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.code_signature.subject_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.code_signature.trusted\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.code_signature.valid\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.entry_modified\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.code_page\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.collection\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.collection.hash.md5\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.collection.hash.sha1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.collection.hash.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.collection.hash.sha512\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.errors\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.errors.count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.errors.error_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.file_extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.project_file\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.project_file.hash.md5\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.project_file.hash.sha1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.project_file.hash.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.project_file.hash.sha512\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.stream\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.stream.hash.md5\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.stream.hash.sha1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.stream.hash.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.stream.hash.sha512\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.stream.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.stream.raw_code\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.macro.stream.raw_code_size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.malware_classification.features\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"file.Ext.malware_classification.features.data.buffer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.malware_classification.features.data.decompressed_size\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.malware_classification.features.data.encoding\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.malware_classification.identifier\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.malware_classification.score\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.malware_classification.threshold\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.malware_classification.upx_packed\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.malware_classification.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.original\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.original.gid\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.original.group\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.original.mode\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.original.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.original.owner\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.original.path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.original.uid\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.quarantine_path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.quarantine_result\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.temp_file_path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.windows\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.windows.zone_identifier\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.accessed\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.attributes\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.created\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.ctime\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.device\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.directory\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.drive_letter\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.gid\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.group\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.hash.md5\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.hash.sha1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.hash.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.hash.sha512\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.inode\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.mime_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.mode\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.mtime\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.owner\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.path.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.path.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"file.pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.size\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.target_path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.target_path.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.target_path.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"file.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.uid\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"group.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"group.Ext.real\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"group.Ext.real.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"group.Ext.real.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"group.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"group.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"group.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.architecture\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.city_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.continent_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.country_iso_code\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.country_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.location\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.region_iso_code\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.geo.region_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.hostname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.mac\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.Ext.variant\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.family\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.full\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.full.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.full.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"host.os.kernel\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.name.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.name.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"host.os.platform\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.uptime\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.Ext.real\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.Ext.real.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.Ext.real.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.email\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.full_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.full_name.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"host.user.group.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.group.Ext.real\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.group.Ext.real.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.group.Ext.real.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.group.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.group.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.group.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.hash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.user.name.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"process.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.ancestry\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.architecture\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.authentication_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.code_signature\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.code_signature.exists\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.code_signature.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.code_signature.subject_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.code_signature.trusted\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.code_signature.valid\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.Ext.code_signature\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.Ext.code_signature.exists\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.Ext.code_signature.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.Ext.code_signature.subject_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.Ext.code_signature.trusted\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.Ext.code_signature.valid\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.Ext.compile_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.Ext.mapped_address\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.Ext.mapped_size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.hash.md5\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.hash.sha1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.hash.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.hash.sha512\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.dll.pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.malware_classification.features\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"process.Ext.malware_classification.features.data.buffer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.malware_classification.features.data.decompressed_size\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.malware_classification.features.data.encoding\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.malware_classification.identifier\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.malware_classification.score\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.malware_classification.threshold\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.malware_classification.upx_packed\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.malware_classification.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.services\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.session\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.token.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.token.elevation\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.token.elevation_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.token.impersonation_level\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.token.integrity_level\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.token.integrity_level_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.token.is_appcontainer\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.token.privileges\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.token.privileges.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.token.privileges.enabled\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.token.privileges.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.token.sid\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.token.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.token.user\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.user\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.args\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.args_count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.command_line\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.command_line.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.command_line.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"process.entity_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.executable\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.executable.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.executable.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"process.exit_code\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.hash.md5\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.hash.sha1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.hash.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.hash.sha512\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.name.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.name.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.architecture\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.code_signature\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.code_signature.exists\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.code_signature.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.code_signature.subject_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.code_signature.trusted\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.code_signature.valid\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.Ext.code_signature\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.Ext.code_signature.exists\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.Ext.code_signature.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.Ext.code_signature.subject_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.Ext.code_signature.trusted\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.Ext.code_signature.valid\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.Ext.compile_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.Ext.mapped_address\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.Ext.mapped_size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.hash.md5\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.hash.sha1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.hash.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.hash.sha512\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.dll.pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.real\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.real.pid\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.token.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.token.elevation\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.token.elevation_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.token.impersonation_level\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.token.integrity_level\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.token.integrity_level_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.token.is_appcontainer\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.token.privileges\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.token.privileges.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.token.privileges.enabled\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.token.privileges.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.token.sid\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.token.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.token.user\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.Ext.user\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.args\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.args_count\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.command_line\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.command_line.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.command_line.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"process.parent.entity_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.executable\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.executable.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.executable.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"process.parent.exit_code\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.hash.md5\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.hash.sha1\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.hash.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.hash.sha512\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.name.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.name.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pgid\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.pid\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.ppid\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.start\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.thread.id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.thread.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.title.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"process.parent.uptime\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.working_directory\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.working_directory.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.working_directory.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"process.pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pgid\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.pid\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.ppid\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.start\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.call_stack.instruction_pointer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.call_stack.memory_section.address\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.call_stack.memory_section.protection\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.call_stack.memory_section.size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.call_stack.module_path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.call_stack.rva\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.call_stack.symbol_info\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.parameter\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.parameter_bytes_compressed\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.parameter_bytes_compressed_present\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.service\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_allocation_offset\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_bytes\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_bytes_disasm\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_bytes_disasm_hash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.allocation_base\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.allocation_protection\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.allocation_size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.allocation_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.bytes_address\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.bytes_allocation_offset\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.bytes_compressed\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.bytes_compressed_present\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.mapped_pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.mapped_pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.mapped_pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.mapped_pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.mapped_pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.mapped_pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.mapped_pe_detected\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.memory_pe.company\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.memory_pe.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.memory_pe.file_version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.memory_pe.imphash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.memory_pe.original_file_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.memory_pe.product\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.memory_pe_detected\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.region_base\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.region_protection\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.region_size\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.region_state\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_details.strings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.start_address_module\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.token.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.token.elevation\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.token.elevation_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.token.impersonation_level\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.token.integrity_level\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.token.integrity_level_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.token.is_appcontainer\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.token.privileges\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.token.privileges.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.token.privileges.enabled\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.token.privileges.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.token.sid\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.token.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.token.user\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.Ext.uptime\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.thread.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.title.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"process.uptime\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.working_directory\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.working_directory.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.working_directory.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"rule.author\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.category\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.license\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.reference\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.ruleset\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.uuid\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"rule.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.city_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.continent_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.country_iso_code\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.country_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.location\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.region_iso_code\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.geo.region_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.framework\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.tactic.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.tactic.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.tactic.reference\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"threat.technique.name.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"threat.technique.reference\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.Ext.real\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.Ext.real.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.Ext.real.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.email\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.full_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.full_name.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"user.group.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.group.Ext.real\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.group.Ext.real.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.group.Ext.real.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.group.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.group.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.group.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.hash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user.name.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"event.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.Ext.correlation\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.Ext.correlation.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.entropy\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"file.Ext.header_data\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"file.Ext.monotonic_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.Ext.load_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.exists\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.subject_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.trusted\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dll.code_signature.valid\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.address\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.packets\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.port\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.registered_domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"destination.top_level_domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.Ext.options\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.Ext.status\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.registered_domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.subdomain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.top_level_domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.question.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"dns.resolved_ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.body.bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.body.content\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.request.body.content.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"http.request.bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.Ext.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.body.bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.body.content\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.body.content.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"http.response.bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"http.response.status_code\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.community_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.direction\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.iana_number\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.packets\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.protocol\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.transport\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"network.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.address\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.packets\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.port\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.registered_domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"source.top_level_domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"package.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.Ext.defense_evasions\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.exists\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.subject_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.trusted\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.code_signature.valid\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.exists\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.subject_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.trusted\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"process.parent.code_signature.valid\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.data.bytes\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.data.strings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.hive\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.key\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.path\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"registry.value\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]","timeFieldName":"@timestamp","title":"logs-*"},"coreMigrationVersion":"7.13.2","id":"logs-*","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"sort":[1623693556928,728],"type":"index-pattern","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MTEsNF0="} -{"attributes":{"description":"this is a logstash saved query","filters":[],"query":{"language":"kuery","query":"extension.raw :\"gif\" and machine.os.raw :\"ios\" "},"timefilter":{"from":"2015-09-20T01:56:56.132Z","refreshInterval":{"pause":true,"value":0},"to":"2015-09-21T11:18:20.471Z"},"title":"logstash_saved_query"},"coreMigrationVersion":"7.13.2","id":"logstash_saved_query","references":[],"sort":[1623693556928,729],"type":"query","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MTIsNF0="} -{"attributes":{"allowNoIndex":true,"fieldFormatMap":"{\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"}}","fields":"[{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"agent.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"data_stream.dataset\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"data_stream.namespace\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"data_stream.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ecs.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"elastic.agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"elastic.agent.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.action\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.category\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.code\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.created\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.dataset\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.hash\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.ingested\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.kind\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.module\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.outcome\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.provider\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.sequence\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.severity\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.architecture\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.domain\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.hostname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.mac\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.Ext\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.Ext.variant\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.family\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.full\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.full.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.full.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"host.os.kernel\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.name.caseless\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.name.text\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"host.os.platform\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.os.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host.uptime\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.cpu\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.cpu.endpoint\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.cpu.endpoint.histogram\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.cpu.endpoint.latest\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.cpu.endpoint.mean\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.disks\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"Endpoint.metrics.disks.device\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.disks.endpoint_drive\",\"type\":\"boolean\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.disks.free\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.disks.fstype\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.disks.mount\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.disks.total\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.memory\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.memory.endpoint\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.memory.endpoint.private\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.memory.endpoint.private.latest\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.memory.endpoint.private.mean\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.threads\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"Endpoint.metrics.uptime\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.uptime.endpoint\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.metrics.uptime.system\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.end\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"event.start\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.actions\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.actions.message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.actions.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.actions.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"Endpoint.policy.applied.artifacts.global\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.global.identifiers\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.global.identifiers.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.global.identifiers.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.global.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.user\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.user.identifiers\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.user.identifiers.name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.user.identifiers.sha256\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.artifacts.user.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.configurations\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"Endpoint.policy.applied.configurations.antivirus_registration\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"Endpoint.policy.applied.configurations.antivirus_registration.concerned_actions\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.configurations.antivirus_registration.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.configurations.events\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.configurations.events.concerned_actions\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.configurations.events.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.configurations.logging\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.configurations.logging.concerned_actions\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.configurations.logging.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.configurations.malware\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.configurations.malware.concerned_actions\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.configurations.malware.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.configurations.streaming\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.configurations.streaming.concerned_actions\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.configurations.streaming.status\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"Endpoint.policy.applied.response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"Endpoint.policy.applied.version\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]","timeFieldName":"@timestamp","title":"metrics-*"},"coreMigrationVersion":"7.13.2","id":"metrics-*","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"sort":[1623693556928,730],"type":"index-pattern","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MTMsNF0="} -{"attributes":{"description":"Shakespeare query","filters":[],"query":{"language":"kuery","query":"speaker : \"OTHELLO\" and play_name :\"Othello\" "},"title":"shakespeare_current_query"},"coreMigrationVersion":"7.13.2","id":"shakespeare_current_query","references":[],"sort":[1623693556928,731],"type":"query","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MTQsNF0="} -{"attributes":{"@created":"2021-05-27T18:53:18.432Z","@timestamp":"2021-05-27T19:46:12.539Z","assets":{},"colors":["#37988d","#c19628","#b83c6f","#3f9939","#1785b0","#ca5f35","#45bdb0","#f2bc33","#e74b8b","#4fbf48","#1ea6dc","#fd7643","#72cec3","#f5cc5d","#ec77a8","#7acf74","#4cbce4","#fd986f","#a1ded7","#f8dd91","#f2a4c5","#a6dfa2","#86d2ed","#fdba9f","#000000","#444444","#777777","#BBBBBB","#FFFFFF","rgba(255,255,255,0)"],"css":".canvasPage {\n\n}","height":720,"isWriteable":true,"name":"logstash-canvas-workpad","page":1,"pages":[{"elements":[{"expression":"savedLens id=\"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781\" timerange={timerange from=\"now-15y\" to=\"now\"}\n| render","filter":null,"id":"element-56d2ba72-f227-4d04-9478-a1d6f0c7e601","position":{"angle":0,"height":300,"left":20,"parent":null,"top":20,"width":500}},{"expression":"savedVisualization id=\"0d8a8860-623a-11eb-aebf-c306684b328d\" timerange={timerange from=\"now-15y\" to=\"now\"}\n| render","filter":null,"id":"element-afbaa26e-10e7-47d4-bb41-b061dfdced2b","position":{"angle":0,"height":300,"left":527,"parent":null,"top":20,"width":500}}],"groups":[],"id":"page-0f9ef2da-2868-4c0b-9223-fd3c9e53d6c9","style":{"background":"#FFF"},"transition":{}},{"elements":[{"expression":"image dataurl=null mode=\"contain\"\n| render","id":"element-c5534ef7-68c4-46bc-b35a-9e43a7f118c3","position":{"angle":0,"height":107,"left":20,"parent":null,"top":20,"width":132}},{"expression":"filters\n| essql query=\"SELECT machine.os.raw FROM \\\"logstash-*\\\"\"\n| pointseries x=\"machine.os.raw\" y=\"size(machine.os.raw)\" color=\"machine.os.raw\" size=\"sum(machine.os.raw)\"\n| plot defaultStyle={seriesStyle points=5 fill=1}\n| render","id":"element-5f7a3312-0e77-471c-9b8f-f98cb38075fb","position":{"angle":0,"height":192,"left":221,"parent":null,"top":56,"width":451}},{"expression":"timefilterControl compact=true column=@timestamp\n| render","filter":"timefilter from=\"now-29y\" to=now column=@timestamp","id":"element-6e00dcf4-06fe-4bd9-9315-d32d9d3fac5f","position":{"angle":0,"height":50,"left":221,"parent":null,"top":-1,"width":500}},{"expression":"filters\n| esdocs index=\"logstash-*\" fields=\"@timestamp, response.raw\"\n| pointseries x=\"size(response.raw)\" y=\"@timestamp\" color=\"response.raw\"\n| plot\n| render","id":"element-20281fac-1c3a-4ee3-9132-44379fb60b74","position":{"angle":0,"height":262,"left":51,"parent":null,"top":304,"width":590}},{"expression":"filters\n| timelion query=\".es(index=logstash-*, metric=sum:bytes)\"\n| pointseries x=\"@timestamp\" y=\"sum(value)\"\n| plot defaultStyle={seriesStyle lines=3}\n| render","id":"element-337b0548-5d6d-44cd-a324-eb50d63c7bd0","position":{"angle":0,"height":309,"left":648,"parent":null,"top":290,"width":369}},{"expression":"savedLens id=\"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781\" timerange={timerange from=\"now-15y\" to=\"now\"}\n| render","filter":null,"id":"element-353e5583-0dbb-4a6b-bac7-3b2a6b305397","position":{"angle":0,"height":181.99999999999997,"left":855,"parent":"group-d2618a19-3982-414e-93df-b2cb165b7c7e","top":15.000000000000014,"width":76.961271102284}},{"expression":"savedVisualization id=\"0d8a8860-623a-11eb-aebf-c306684b328d\" timerange={timerange from=\"now-15y\" to=\"now\"}\n| render","filter":null,"id":"element-0e5501a6-9e87-42bc-b539-1e697e62051b","position":{"angle":0,"height":181.99999999999997,"left":933.038728897716,"parent":"group-d2618a19-3982-414e-93df-b2cb165b7c7e","top":15.000000000000014,"width":76.961271102284}}],"groups":[],"id":"page-59c3cf09-1811-4324-995b-7336c1c11ab8","style":{"background":"#FFF"},"transition":{}}],"variables":[],"width":1080},"coreMigrationVersion":"7.13.2","id":"workpad-f2024ca3-e366-447a-b3af-7db4400646ef","migrationVersion":{"canvas-workpad":"7.0.0"},"references":[],"sort":[1623693556928,732],"type":"canvas-workpad","updated_at":"2021-06-14T17:59:16.928Z","version":"WzE3MTUsNF0="} -{"exportedCount":87,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file +{"attributes":{"accessCount":0,"accessDate":1621977234367,"createDate":1621977234367,"url":"/app/dashboards#/view/154944b0-6249-11eb-aebf-c306684b328d?embed=true&_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&_a=(description:%27%27,filters:!(),fullScreenMode:!f,options:(darkTheme:!f,hidePanelTitles:!f,useMargins:!t),panels:!((embeddableConfig:(enhancements:()),gridData:(h:15,i:%271%27,w:24,x:0,y:0),id:%2736b91810-6239-11eb-aebf-c306684b328d%27,panelIndex:%271%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:(),vis:!n),gridData:(h:15,i:%272%27,w:24,x:24,y:0),id:%270a274320-61cc-11eb-aebf-c306684b328d%27,panelIndex:%272%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%273%27,w:24,x:0,y:15),id:e4aef350-623d-11eb-aebf-c306684b328d,panelIndex:%273%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%274%27,w:24,x:24,y:15),id:f92e5630-623e-11eb-aebf-c306684b328d,panelIndex:%274%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:(),vis:!n),gridData:(h:15,i:%275%27,w:24,x:0,y:30),id:%279853d4d0-623d-11eb-aebf-c306684b328d%27,panelIndex:%275%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:(),vis:!n),gridData:(h:15,i:%276%27,w:24,x:24,y:30),id:%276ecb33b0-623d-11eb-aebf-c306684b328d%27,panelIndex:%276%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:(),vis:!n),gridData:(h:15,i:%277%27,w:24,x:0,y:45),id:b8e35c80-623c-11eb-aebf-c306684b328d,panelIndex:%277%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%278%27,w:24,x:24,y:45),id:f1bc75d0-6239-11eb-aebf-c306684b328d,panelIndex:%278%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%279%27,w:24,x:0,y:60),id:%270d8a8860-623a-11eb-aebf-c306684b328d%27,panelIndex:%279%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2710%27,w:24,x:24,y:60),id:d79fe3d0-6239-11eb-aebf-c306684b328d,panelIndex:%2710%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2711%27,w:24,x:0,y:75),id:%27318375a0-6240-11eb-aebf-c306684b328d%27,panelIndex:%2711%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2712%27,w:24,x:24,y:75),id:e461eb20-6245-11eb-aebf-c306684b328d,panelIndex:%2712%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2713%27,w:24,x:0,y:90),id:%2725bdc750-6242-11eb-aebf-c306684b328d%27,panelIndex:%2713%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2714%27,w:24,x:24,y:90),id:%2771dd7bc0-6248-11eb-aebf-c306684b328d%27,panelIndex:%2714%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2715%27,w:24,x:0,y:105),id:%276aea48a0-6240-11eb-aebf-c306684b328d%27,panelIndex:%2715%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2716%27,w:24,x:24,y:105),id:%2732b681f0-6241-11eb-aebf-c306684b328d%27,panelIndex:%2716%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2717%27,w:24,x:0,y:120),id:ccca99e0-6244-11eb-aebf-c306684b328d,panelIndex:%2717%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2718%27,w:24,x:24,y:120),id:a4d7be80-6245-11eb-aebf-c306684b328d,panelIndex:%2718%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2719%27,w:24,x:0,y:135),id:c94d8440-6248-11eb-aebf-c306684b328d,panelIndex:%2719%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%2720%27,w:24,x:24,y:135),id:db6226f0-61c0-11eb-aebf-c306684b328d,panelIndex:%2720%27,type:search,version:%277.13.1%27)),query:(language:lucene,query:%27%27),tags:!(),timeRestore:!f,title:logstash_dashboardwithtime,viewMode:view)"},"coreMigrationVersion":"7.13.5","id":"058bc10f0578013fc41ddedc9a1dcd1e","references":[],"sort":[1651599812857,5],"type":"url","updated_at":"2022-05-03T17:43:32.857Z","version":"WzE3LDFd"} +{"attributes":{"color":"#ba898f","description":"","name":"By value tag"},"coreMigrationVersion":"7.13.5","id":"07f48f70-ca29-11eb-bf5e-3de94e83d4f0","references":[],"sort":[1651599812857,6],"type":"tag","updated_at":"2022-05-03T17:43:32.857Z","version":"WzE4LDFd"} +{"attributes":{"fieldAttrs":"{\"machine.os\":{\"count\":1},\"spaces\":{\"count\":1},\"type\":{\"count\":1},\"bytes_scripted\":{\"count\":1}}","fields":"[{\"count\":1,\"script\":\"doc['bytes'].value*1024\",\"lang\":\"painless\",\"name\":\"bytes_scripted\",\"type\":\"number\",\"scripted\":true,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false}]","runtimeFieldMap":"{}","timeFieldName":"@timestamp","title":"logstash-*"},"coreMigrationVersion":"7.13.5","id":"56b34100-619d-11eb-aebf-c306684b328d","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"sort":[1651599812857,7],"type":"index-pattern","updated_at":"2022-05-03T17:43:32.857Z","version":"WzIyLDFd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.5\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"60aaea59-d871-4e90-9ff3-78946d6bef90\"},\"panelIndex\":\"60aaea59-d871-4e90-9ff3-78946d6bef90\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsPie\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"2174c5fe-75d2-43ae-9c79-1a7cc7bbbaea\":{\"columns\":{\"65625f0d-e7f1-4370-b939-7db27af74de7\":{\"label\":\"Top values of geo.srcdest\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"geo.srcdest\",\"isBucketed\":true,\"params\":{\"size\":18,\"orderBy\":{\"type\":\"column\",\"columnId\":\"553a353f-dac5-4a52-a25d-52c6e1462597\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"553a353f-dac5-4a52-a25d-52c6e1462597\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"}},\"columnOrder\":[\"65625f0d-e7f1-4370-b939-7db27af74de7\",\"553a353f-dac5-4a52-a25d-52c6e1462597\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"shape\":\"donut\",\"layers\":[{\"layerId\":\"2174c5fe-75d2-43ae-9c79-1a7cc7bbbaea\",\"groups\":[\"65625f0d-e7f1-4370-b939-7db27af74de7\",\"65625f0d-e7f1-4370-b939-7db27af74de7\",\"65625f0d-e7f1-4370-b939-7db27af74de7\"],\"metric\":\"553a353f-dac5-4a52-a25d-52c6e1462597\",\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"default\",\"nestedLegend\":false}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-2174c5fe-75d2-43ae-9c79-1a7cc7bbbaea\"}]},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"9ed45b8b-534b-4fac-9fee-436896b90039\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"circular drilldown\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}}}]","timeRestore":false,"title":"lens_panel_drilldown","version":1},"coreMigrationVersion":"7.13.5","id":"35ce3b30-ca29-11eb-bf5e-3de94e83d4f0","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"60aaea59-d871-4e90-9ff3-78946d6bef90:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"60aaea59-d871-4e90-9ff3-78946d6bef90:indexpattern-datasource-layer-2174c5fe-75d2-43ae-9c79-1a7cc7bbbaea","type":"index-pattern"},{"id":"08dec860-ca29-11eb-bf5e-3de94e83d4f0","name":"60aaea59-d871-4e90-9ff3-78946d6bef90:drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:9ed45b8b-534b-4fac-9fee-436896b90039:dashboardId","type":"dashboard"},{"id":"07f48f70-ca29-11eb-bf5e-3de94e83d4f0","name":"tag-07f48f70-ca29-11eb-bf5e-3de94e83d4f0","type":"tag"}],"sort":[1651613174194,1151],"type":"dashboard","updated_at":"2022-05-03T21:26:14.194Z","version":"WzcxMywxXQ=="} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[{\"meta\":{\"alias\":null,\"disabled\":false,\"key\":\"geo.srcdest\",\"negate\":true,\"params\":[\"CN:CN\",\"IN:CN\",\"CN:IN\",\"IN:IN\",\"CN:US\",\"IN:US\",\"US:IN\",\"US:CN\",\"ID:CN\",\"US:US\",\"CN:ID\",\"PK:CN\",\"BR:CN\",\"CN:PK\",\"BD:CN\",\"CN:BR\",\"IN:PK\",\"NG:CN\"],\"type\":\"phrases\",\"value\":\"CN:CN, IN:CN, CN:IN, IN:IN, CN:US, IN:US, US:IN, US:CN, ID:CN, US:US, CN:ID, PK:CN, BR:CN, CN:PK, BD:CN, CN:BR, IN:PK, NG:CN\",\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"bool\":{\"minimum_should_match\":1,\"should\":[{\"match_phrase\":{\"geo.srcdest\":\"CN:CN\"}},{\"match_phrase\":{\"geo.srcdest\":\"IN:CN\"}},{\"match_phrase\":{\"geo.srcdest\":\"CN:IN\"}},{\"match_phrase\":{\"geo.srcdest\":\"IN:IN\"}},{\"match_phrase\":{\"geo.srcdest\":\"CN:US\"}},{\"match_phrase\":{\"geo.srcdest\":\"IN:US\"}},{\"match_phrase\":{\"geo.srcdest\":\"US:IN\"}},{\"match_phrase\":{\"geo.srcdest\":\"US:CN\"}},{\"match_phrase\":{\"geo.srcdest\":\"ID:CN\"}},{\"match_phrase\":{\"geo.srcdest\":\"US:US\"}},{\"match_phrase\":{\"geo.srcdest\":\"CN:ID\"}},{\"match_phrase\":{\"geo.srcdest\":\"PK:CN\"}},{\"match_phrase\":{\"geo.srcdest\":\"BR:CN\"}},{\"match_phrase\":{\"geo.srcdest\":\"CN:PK\"}},{\"match_phrase\":{\"geo.srcdest\":\"BD:CN\"}},{\"match_phrase\":{\"geo.srcdest\":\"CN:BR\"}},{\"match_phrase\":{\"geo.srcdest\":\"IN:PK\"}},{\"match_phrase\":{\"geo.srcdest\":\"NG:CN\"}}]}},\"$state\":{\"store\":\"appState\"}}]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.5\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"095e13b2-d0ac-47db-a62b-0aca28931402\"},\"panelIndex\":\"095e13b2-d0ac-47db-a62b-0aca28931402\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"f61694eb-94ed-495d-9ce8-63592f040b0b\":{\"columns\":{\"75ddcdb4-3050-4545-b401-509384b0d532\":{\"label\":\"Top values of machine.os.raw\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"machine.os.raw\",\"isBucketed\":true,\"params\":{\"size\":3,\"orderBy\":{\"type\":\"column\",\"columnId\":\"2f97b0f5-f0ff-40f2-abb0-bb7d1081a126\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"273e31ef-7c2d-4d0e-9063-5528f4011a51\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\"}},\"2f97b0f5-f0ff-40f2-abb0-bb7d1081a126\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"},\"2eb30654-0ead-40ac-92ab-d8d113e25ac5\":{\"label\":\"Average of bytes\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"bytes\",\"isBucketed\":false,\"scale\":\"ratio\"}},\"columnOrder\":[\"75ddcdb4-3050-4545-b401-509384b0d532\",\"273e31ef-7c2d-4d0e-9063-5528f4011a51\",\"2f97b0f5-f0ff-40f2-abb0-bb7d1081a126\",\"2eb30654-0ead-40ac-92ab-d8d113e25ac5\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"f61694eb-94ed-495d-9ce8-63592f040b0b\",\"accessors\":[\"2f97b0f5-f0ff-40f2-abb0-bb7d1081a126\",\"2eb30654-0ead-40ac-92ab-d8d113e25ac5\"],\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"xAccessor\":\"273e31ef-7c2d-4d0e-9063-5528f4011a51\",\"splitAccessor\":\"75ddcdb4-3050-4545-b401-509384b0d532\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-f61694eb-94ed-495d-9ce8-63592f040b0b\"}]},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"da7ad2b6-1e4a-40b5-9123-d0ec2bde858d\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"lens_panel_drilldown\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}}},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"a254a623-a9af-4372-851b-572fa95b0902\"},\"panelIndex\":\"a254a623-a9af-4372-851b-572fa95b0902\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"vega\",\"params\":{\"spec\":\"{\\n/*\\n\\nWelcome to Vega visualizations. Here you can design your own dataviz from scratch using a declarative language called Vega, or its simpler form Vega-Lite. In Vega, you have the full control of what data is loaded, even from multiple sources, how that data is transformed, and what visual elements are used to show it. Use help icon to view Vega examples, tutorials, and other docs. Use the wrench icon to reformat this text, or to remove comments.\\n\\nThis example graph shows the document count in all indexes in the current time range. You might need to adjust the time filter in the upper right corner.\\n*/\\n\\n $schema: https://vega.github.io/schema/vega-lite/v4.json\\n title: Event counts from all indexes\\n\\n // Define the data source\\n data: {\\n url: {\\n/*\\nAn object instead of a string for the \\\"url\\\" param is treated as an Elasticsearch query. Anything inside this object is not part of the Vega language, but only understood by Kibana and Elasticsearch server. This query counts the number of documents per time interval, assuming you have a @timestamp field in your data.\\n\\nKibana has a special handling for the fields surrounded by \\\"%\\\". They are processed before the the query is sent to Elasticsearch. This way the query becomes context aware, and can use the time range and the dashboard filters.\\n*/\\n\\n // Apply dashboard context filters when set\\n %context%: true\\n // Filter the time picker (upper right corner) with this field\\n %timefield%: @timestamp\\n\\n/*\\nSee .search() documentation for : https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/api-reference.html#api-search\\n*/\\n\\n // Which index to search\\n index: _all\\n // Aggregate data by the time field into time buckets, counting the number of documents in each bucket.\\n body: {\\n aggs: {\\n time_buckets: {\\n date_histogram: {\\n // Use date histogram aggregation on @timestamp field\\n field: @timestamp\\n // The interval value will depend on the daterange picker (true), or use an integer to set an approximate bucket count\\n interval: {%autointerval%: true}\\n // Make sure we get an entire range, even if it has no data\\n extended_bounds: {\\n // Use the current time range's start and end\\n min: {%timefilter%: \\\"min\\\"}\\n max: {%timefilter%: \\\"max\\\"}\\n }\\n // Use this for linear (e.g. line, area) graphs. Without it, empty buckets will not show up\\n min_doc_count: 0\\n }\\n }\\n }\\n // Speed up the response by only including aggregation results\\n size: 0\\n }\\n }\\n/*\\nElasticsearch will return results in this format:\\n\\naggregations: {\\n time_buckets: {\\n buckets: [\\n {\\n key_as_string: 2015-11-30T22:00:00.000Z\\n key: 1448920800000\\n doc_count: 0\\n },\\n {\\n key_as_string: 2015-11-30T23:00:00.000Z\\n key: 1448924400000\\n doc_count: 0\\n }\\n ...\\n ]\\n }\\n}\\n\\nFor our graph, we only need the list of bucket values. Use the format.property to discard everything else.\\n*/\\n format: {property: \\\"aggregations.time_buckets.buckets\\\"}\\n }\\n\\n // \\\"mark\\\" is the graphics element used to show our data. Other mark values are: area, bar, circle, line, point, rect, rule, square, text, and tick. See https://vega.github.io/vega-lite/docs/mark.html\\n mark: line\\n\\n // \\\"encoding\\\" tells the \\\"mark\\\" what data to use and in what way. See https://vega.github.io/vega-lite/docs/encoding.html\\n encoding: {\\n x: {\\n // The \\\"key\\\" value is the timestamp in milliseconds. Use it for X axis.\\n field: key\\n type: temporal\\n axis: {title: false} // Customize X axis format\\n }\\n y: {\\n // The \\\"doc_count\\\" is the count per bucket. Use it for Y axis.\\n field: doc_count\\n type: quantitative\\n axis: {title: \\\"Document count\\\"}\\n }\\n }\\n}\\n\"},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[{\"$state\":{\"store\":\"appState\"},\"meta\":{\"alias\":null,\"disabled\":false,\"index\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"key\":\"geo.srcdest\",\"negate\":false,\"params\":{\"query\":\"CN:CN\"},\"type\":\"phrase\"},\"query\":{\"match_phrase\":{\"geo.srcdest\":\"CN:CN\"}}}]}}},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"cfd2521d-15a0-4c64-b0ab-d2dc18f396e3\",\"triggers\":[\"CONTEXT_MENU_TRIGGER\"],\"action\":{\"name\":\"URL\",\"config\":{\"url\":{\"template\":\"http://localhost:5601/app/discover#/view/4acce030-ca2a-11eb-bf5e-3de94e83d4f0?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&_a=(columns:!(),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'43fcac20-ca27-11eb-bf5e-3de94e83d4f0',key:ip,negate:!f,params:(query:'57.237.11.219'),type:phrase),query:(match_phrase:(ip:'57.237.11.219')))),index:'43fcac20-ca27-11eb-bf5e-3de94e83d4f0',interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))\"},\"openInNewTab\":true,\"encodeUrl\":true},\"factoryId\":\"URL_DRILLDOWN\"}}]}}}},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"5de83d82-bbd1-4d30-be61-dd6724f32c07\"},\"panelIndex\":\"5de83d82-bbd1-4d30-be61-dd6724f32c07\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"metrics\",\"params\":{\"annotations\":[{\"fields\":\"response.raw\",\"template\":\"{{response.raw}}\",\"index_pattern\":\"logstash-*\",\"query_string\":{\"query\":\"response.raw :\\\"404\\\" \",\"language\":\"kuery\"},\"color\":\"#F00\",\"icon\":\"fa-bomb\",\"id\":\"37395960-ca28-11eb-9eac-2f3ccefcbeef\",\"ignore_global_filters\":1,\"ignore_panel_filters\":1,\"time_field\":\"@timestamp\"}],\"axis_formatter\":\"number\",\"axis_position\":\"left\",\"axis_scale\":\"normal\",\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"index_pattern\":\"logstash-*\",\"interval\":\"\",\"isModelInvalid\":false,\"series\":[{\"axis_position\":\"right\",\"chart_type\":\"line\",\"color\":\"#68BC00\",\"fill\":0.5,\"formatter\":\"number\",\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"line_width\":1,\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"count\"}],\"point_size\":1,\"separate_axis\":0,\"split_color_mode\":\"kibana\",\"split_mode\":\"everything\",\"stacked\":\"none\"}],\"show_grid\":1,\"show_legend\":1,\"time_field\":\"@timestamp\",\"tooltip_mode\":\"show_all\",\"type\":\"timeseries\",\"use_kibana_indexes\":false},\"uiState\":{},\"data\":{\"aggs\":[],\"searchSource\":{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"23d04651-266f-4f0a-8eef-6f190f0a84af\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"dashboard\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}}},{\"version\":\"7.13.5\",\"type\":\"map\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"ac0b1e52-de90-4c93-864b-32ce338fef51\"},\"panelIndex\":\"ac0b1e52-de90-4c93-864b-32ce338fef51\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"layerListJSON\":\"[{\\\"sourceDescriptor\\\":{\\\"type\\\":\\\"EMS_TMS\\\",\\\"isAutoSelect\\\":true},\\\"id\\\":\\\"0965c5ce-6347-43f2-917e-b938ea3862f8\\\",\\\"label\\\":null,\\\"minZoom\\\":0,\\\"maxZoom\\\":24,\\\"alpha\\\":1,\\\"visible\\\":true,\\\"style\\\":{\\\"type\\\":\\\"TILE\\\"},\\\"type\\\":\\\"VECTOR_TILE\\\"},{\\\"sourceDescriptor\\\":{\\\"indexPatternId\\\":\\\"56b34100-619d-11eb-aebf-c306684b328d\\\",\\\"geoField\\\":\\\"geo.coordinates\\\",\\\"filterByMapBounds\\\":true,\\\"scalingType\\\":\\\"CLUSTERS\\\",\\\"id\\\":\\\"8fcc6cc4-919b-418c-9815-5a002cfca117\\\",\\\"type\\\":\\\"ES_SEARCH\\\",\\\"applyGlobalQuery\\\":true,\\\"applyGlobalTime\\\":true,\\\"tooltipProperties\\\":[],\\\"sortField\\\":\\\"\\\",\\\"sortOrder\\\":\\\"desc\\\",\\\"topHitsSplitField\\\":\\\"\\\",\\\"topHitsSize\\\":1},\\\"id\\\":\\\"0b28253c-ed9e-401f-9306-feb0e58d1c0e\\\",\\\"label\\\":\\\"Clustered logstash-*\\\",\\\"minZoom\\\":0,\\\"maxZoom\\\":24,\\\"alpha\\\":0.75,\\\"visible\\\":true,\\\"style\\\":{\\\"type\\\":\\\"VECTOR\\\",\\\"properties\\\":{\\\"icon\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"value\\\":\\\"marker\\\"}},\\\"fillColor\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"color\\\":\\\"#54B399\\\"}},\\\"lineColor\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"color\\\":\\\"#41937c\\\"}},\\\"lineWidth\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"size\\\":1}},\\\"iconSize\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"size\\\":6}},\\\"iconOrientation\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"orientation\\\":0}},\\\"labelText\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"value\\\":\\\"\\\"}},\\\"labelColor\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"color\\\":\\\"#000000\\\"}},\\\"labelSize\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"size\\\":14}},\\\"labelBorderColor\\\":{\\\"type\\\":\\\"STATIC\\\",\\\"options\\\":{\\\"color\\\":\\\"#FFFFFF\\\"}},\\\"symbolizeAs\\\":{\\\"options\\\":{\\\"value\\\":\\\"circle\\\"}},\\\"labelBorderSize\\\":{\\\"options\\\":{\\\"size\\\":\\\"SMALL\\\"}}},\\\"isTimeAware\\\":true},\\\"type\\\":\\\"BLENDED_VECTOR\\\",\\\"joins\\\":[]}]\",\"mapStateJSON\":\"{\\\"zoom\\\":1.45,\\\"center\\\":{\\\"lon\\\":0,\\\"lat\\\":19.94277},\\\"timeFilters\\\":{\\\"from\\\":\\\"2015-09-20T01:56:56.132Z\\\",\\\"to\\\":\\\"2015-09-21T11:18:20.471Z\\\"},\\\"refreshConfig\\\":{\\\"isPaused\\\":true,\\\"interval\\\":0},\\\"query\\\":{\\\"query\\\":\\\"\\\",\\\"language\\\":\\\"kuery\\\"},\\\"filters\\\":[],\\\"settings\\\":{\\\"autoFitToDataBounds\\\":false,\\\"backgroundColor\\\":\\\"#ffffff\\\",\\\"disableInteractive\\\":false,\\\"disableTooltipControl\\\":false,\\\"hideToolbarOverlay\\\":false,\\\"hideLayerControl\\\":false,\\\"hideViewControl\\\":false,\\\"initialLocation\\\":\\\"LAST_SAVED_LOCATION\\\",\\\"fixedLocation\\\":{\\\"lat\\\":0,\\\"lon\\\":0,\\\"zoom\\\":2},\\\"browserLocation\\\":{\\\"zoom\\\":2},\\\"maxZoom\\\":24,\\\"minZoom\\\":0,\\\"showScaleControl\\\":false,\\\"showSpatialFilters\\\":true,\\\"spatialFiltersAlpa\\\":0.3,\\\"spatialFiltersFillColor\\\":\\\"#DA8B45\\\",\\\"spatialFiltersLineColor\\\":\\\"#DA8B45\\\"}}\",\"uiStateJSON\":\"{\\\"isLayerTOCOpen\\\":true,\\\"openTOCDetails\\\":[]}\"},\"mapCenter\":{\"lat\":-24.21784,\"lon\":4.3751,\"zoom\":1.45},\"mapBuffer\":{\"minLon\":-222.61538000000002,\"minLat\":-101.36965000000001,\"maxLon\":231.36558,\"maxLat\":65.74483000000001},\"isLayerTOCOpen\":true,\"openTOCDetails\":[],\"hiddenLayers\":[],\"enhancements\":{}}},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"147049a3-5826-48e6-9514-17078f94b6bd\"},\"panelIndex\":\"147049a3-5826-48e6-9514-17078f94b6bd\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"significant_terms\",\"params\":{\"field\":\"geo.srcdest\",\"size\":77},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"enhancements\":{}}}]","timeRestore":false,"title":"logstash_by_value_dashboard","version":1},"coreMigrationVersion":"7.13.5","id":"08dec860-ca29-11eb-bf5e-3de94e83d4f0","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"095e13b2-d0ac-47db-a62b-0aca28931402:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"095e13b2-d0ac-47db-a62b-0aca28931402:indexpattern-datasource-layer-f61694eb-94ed-495d-9ce8-63592f040b0b","type":"index-pattern"},{"id":"35ce3b30-ca29-11eb-bf5e-3de94e83d4f0","name":"095e13b2-d0ac-47db-a62b-0aca28931402:drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:da7ad2b6-1e4a-40b5-9123-d0ec2bde858d:dashboardId","type":"dashboard"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"a254a623-a9af-4372-851b-572fa95b0902:kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"},{"id":"35ce3b30-ca29-11eb-bf5e-3de94e83d4f0","name":"5de83d82-bbd1-4d30-be61-dd6724f32c07:drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:23d04651-266f-4f0a-8eef-6f190f0a84af:dashboardId","type":"dashboard"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"ac0b1e52-de90-4c93-864b-32ce338fef51:layer_1_source_index_pattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"147049a3-5826-48e6-9514-17078f94b6bd:kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"07f48f70-ca29-11eb-bf5e-3de94e83d4f0","name":"tag-07f48f70-ca29-11eb-bf5e-3de94e83d4f0","type":"tag"}],"sort":[1651613469669,1400],"type":"dashboard","updated_at":"2022-05-03T21:31:09.669Z","version":"Wzk2NiwxXQ=="} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_scriptedfieldviz","uiStateJSON":"{\"vis\":{\"defaultColors\":{\"0 - 100\":\"rgb(0,104,55)\"}}}","version":1,"visState":"{\"title\":\"logstash_scriptedfieldviz\",\"type\":\"goal\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"isDisplayWarning\":false,\"type\":\"gauge\",\"gauge\":{\"verticalSplit\":false,\"autoExtend\":false,\"percentageMode\":true,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\",\"width\":2},\"type\":\"meter\",\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"range\",\"schema\":\"group\",\"params\":{\"field\":\"bytes_scripted\",\"ranges\":[{\"from\":0,\"to\":40000},{\"from\":40001,\"to\":20000000}]}}]}"},"coreMigrationVersion":"7.13.5","id":"0a274320-61cc-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,9],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzIzLDFd"} +{"attributes":{"columns":[],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[{\"meta\":{\"type\":\"phrases\",\"key\":\"geo.srcdest\",\"value\":\"IN:CN\",\"params\":[\"IN:CN\"],\"alias\":null,\"negate\":false,\"disabled\":false,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"bool\":{\"should\":[{\"match_phrase\":{\"geo.srcdest\":\"IN:CN\"}}],\"minimum_should_match\":1}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["@timestamp","desc"]],"title":"search_saved","version":1},"coreMigrationVersion":"7.13.5","id":"0abce1c0-ca2a-11eb-bf5e-3de94e83d4f0","migrationVersion":{"search":"7.9.3"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"}],"sort":[1651612922671,997],"type":"search","updated_at":"2022-05-03T21:22:02.671Z","version":"WzU4MCwxXQ=="} +{"attributes":{"color":"#81a93f","description":"","name":"logstash_tag"},"coreMigrationVersion":"7.13.5","id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","references":[],"sort":[1651599812857,262],"type":"tag","updated_at":"2022-05-03T17:43:32.857Z","version":"WzI1LDFd"} +{"attributes":{"description":"","layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"4c2394ca-a6a2-4f8d-9631-259eb3a9627f\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"},{\"sourceDescriptor\":{\"geoField\":\"geo.coordinates\",\"filterByMapBounds\":true,\"scalingType\":\"CLUSTERS\",\"id\":\"7555324e-e793-4b7d-a9d2-cd63e6b7fe3d\",\"type\":\"ES_SEARCH\",\"applyGlobalQuery\":true,\"applyGlobalTime\":true,\"tooltipProperties\":[\"geo.srcdest\",\"machine.os\",\"type\"],\"sortField\":\"bytes_scripted\",\"sortOrder\":\"desc\",\"topHitsSplitField\":\"\",\"topHitsSize\":1,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"id\":\"6a493d8b-a220-46bc-8906-a1a7569799e0\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"extension.raw\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":3},\"type\":\"CATEGORICAL\"}},\"lineColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"machine.os.raw\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":3},\"type\":\"CATEGORICAL\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true},\"type\":\"BLENDED_VECTOR\",\"joins\":[]}]","mapStateJSON":"{\"zoom\":1.56,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-15y\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"backgroundColor\":\"#ffffff\",\"disableInteractive\":false,\"disableTooltipControl\":false,\"hideToolbarOverlay\":false,\"hideLayerControl\":false,\"hideViewControl\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showScaleControl\":false,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}","title":"logstash_maps","uiStateJSON":"{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}"},"coreMigrationVersion":"7.13.5","id":"0c5974f0-be5c-11eb-9520-1b4c3ca6a781","migrationVersion":{"map":"7.12.0"},"references":[{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"layer_1_source_index_pattern","type":"index-pattern"}],"sort":[1651599812857,265],"type":"map","updated_at":"2022-05-03T17:43:32.857Z","version":"WzI2LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_datatable","uiStateJSON":"{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}","version":1,"visState":"{\"title\":\"logstash_datatable\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":true,\"totalFunc\":\"sum\",\"showToolbar\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"bucket\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-07-24T08:58:14.175Z\",\"to\":\"2015-11-11T13:28:17.223Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"response.raw\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.5","id":"0d8a8860-623a-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,267],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzI3LDFd"} +{"attributes":{"description":null,"state":{"datasourceStates":{"indexpattern":{"layers":{"35fd070e-5bbc-4906-bf69-8548a213d7a0":{"columnOrder":["2bf7969f-0371-4df2-a398-0a191e428ce5","aab812d6-609b-444d-9990-1e67f85fd85d","e9829e8a-c484-4c9d-b489-f1eb3fb138d2","4fc9fb3b-29a5-4679-ab3c-90d5daaf0661"],"columns":{"2bf7969f-0371-4df2-a398-0a191e428ce5":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"4fc9fb3b-29a5-4679-ab3c-90d5daaf0661":{"dataType":"number","isBucketed":false,"label":"Moving average of Median of bytes","operationType":"moving_average","params":{"window":5},"references":["e9829e8a-c484-4c9d-b489-f1eb3fb138d2"],"scale":"ratio"},"aab812d6-609b-444d-9990-1e67f85fd85d":{"dataType":"number","isBucketed":false,"label":"Average of bytes","operationType":"average","scale":"ratio","sourceField":"bytes"},"e9829e8a-c484-4c9d-b489-f1eb3fb138d2":{"dataType":"number","isBucketed":false,"label":"Median of bytes","operationType":"median","scale":"ratio","sourceField":"bytes"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["aab812d6-609b-444d-9990-1e67f85fd85d","4fc9fb3b-29a5-4679-ab3c-90d5daaf0661"],"layerId":"35fd070e-5bbc-4906-bf69-8548a213d7a0","position":"top","seriesType":"bar_stacked","showGridlines":false,"xAccessor":"2bf7969f-0371-4df2-a398-0a191e428ce5"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_barvertical_stacked_average","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.5","id":"0dbbf8b0-be3c-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-35fd070e-5bbc-4906-bf69-8548a213d7a0","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,16],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"WzI4LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_area_chart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_area_chart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2010-01-28T19:25:55.242Z\",\"to\":\"2021-01-28T19:40:55.242Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"machine.os.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"machine OS\"}}]}"},"coreMigrationVersion":"7.13.5","id":"36b91810-6239-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,18],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzI5LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_horizontal","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_horizontal\",\"type\":\"horizontal_bar\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":200},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":75,\"filter\":true,\"truncate\":100},\"title\":{\"text\":\"no of documents\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"normal\",\"data\":{\"label\":\"no of documents\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":true,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{\"customLabel\":\"no of documents\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-07-24T08:58:14.175Z\",\"to\":\"2015-11-11T13:28:17.223Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"extension.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.5","id":"e4aef350-623d-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,20],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzMwLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_linechart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_linechart\",\"type\":\"line\",\"params\":{\"type\":\"line\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"line\",\"mode\":\"normal\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"radiusRatio\":51,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-09-18T06:38:43.311Z\",\"to\":\"2015-09-26T04:02:51.104Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"sum\",\"schema\":\"radius\",\"params\":{\"field\":\"bytes\",\"customLabel\":\"bubbles\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"machine.os.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.5","id":"f92e5630-623e-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,22],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzMxLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_heatmap","uiStateJSON":"{\"vis\":{\"defaultColors\":{\"0% - 25%\":\"rgb(255,255,204)\",\"25% - 50%\":\"rgb(254,217,118)\",\"50% - 75%\":\"rgb(253,141,60)\",\"75% - 100%\":\"rgb(227,27,28)\"}}}","version":1,"visState":"{\"title\":\"logstash_heatmap\",\"type\":\"heatmap\",\"params\":{\"type\":\"heatmap\",\"addTooltip\":true,\"addLegend\":true,\"enableHover\":false,\"legendPosition\":\"right\",\"times\":[],\"colorsNumber\":4,\"colorSchema\":\"Yellow to Red\",\"setColorRange\":false,\"colorsRange\":[],\"invertColors\":false,\"percentageMode\":true,\"valueAxes\":[{\"show\":false,\"id\":\"ValueAxis-1\",\"type\":\"value\",\"scale\":{\"type\":\"linear\",\"defaultYExtents\":false},\"labels\":{\"show\":false,\"rotate\":0,\"overwriteColor\":false,\"color\":\"#555\"}}],\"row\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-07-24T08:58:14.175Z\",\"to\":\"2015-11-11T13:28:17.223Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"machine.os.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"split\",\"params\":{\"field\":\"response.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.5","id":"9853d4d0-623d-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,24],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzMyLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_goalchart","uiStateJSON":"{\"vis\":{\"defaultColors\":{\"0 - 33\":\"rgb(0,104,55)\",\"33 - 67\":\"rgb(255,255,190)\",\"67 - 100\":\"rgb(165,0,38)\"}}}","version":1,"visState":"{\"title\":\"logstash_goalchart\",\"type\":\"goal\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"isDisplayWarning\":false,\"type\":\"gauge\",\"gauge\":{\"verticalSplit\":false,\"autoExtend\":false,\"percentageMode\":true,\"gaugeType\":\"Circle\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000},{\"from\":10001,\"to\":20000},{\"from\":20001,\"to\":30000}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\",\"width\":2},\"type\":\"meter\",\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60},\"minAngle\":0,\"maxAngle\":6.283185307179586}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"group\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-07-24T08:58:14.175Z\",\"to\":\"2015-11-11T13:28:17.223Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}"},"coreMigrationVersion":"7.13.5","id":"6ecb33b0-623d-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,26],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzMzLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_gauge","uiStateJSON":"{\"vis\":{\"defaultColors\":{\"0 - 50\":\"rgb(0,104,55)\",\"50 - 75\":\"rgb(255,255,190)\",\"75 - 100\":\"rgb(165,0,38)\"}}}","version":1,"visState":"{\"title\":\"logstash_gauge\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":50},{\"from\":50,\"to\":75},{\"from\":75,\"to\":100}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"\",\"fontSize\":60,\"labelColor\":true},\"alignment\":\"horizontal\"}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"range\",\"schema\":\"group\",\"params\":{\"field\":\"bytes\",\"ranges\":[{\"from\":0,\"to\":10001},{\"from\":10002,\"to\":1000000}],\"json\":\"\"}}]}"},"coreMigrationVersion":"7.13.5","id":"b8e35c80-623c-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,28],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzM0LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_coordinatemaps","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_coordinatemaps\",\"type\":\"tile_map\",\"params\":{\"colorSchema\":\"Yellow to Red\",\"mapType\":\"Scaled Circle Markers\",\"isDesaturated\":false,\"addTooltip\":true,\"heatClusterSize\":1.5,\"legendPosition\":\"bottomright\",\"mapZoom\":2,\"mapCenter\":[0,0],\"wms\":{\"enabled\":false,\"options\":{\"format\":\"image/png\",\"transparent\":true},\"selectedTmsLayer\":{\"origin\":\"elastic_maps_service\",\"id\":\"road_map\",\"minZoom\":0,\"maxZoom\":18,\"attribution\":\"

© OpenStreetMap contributors|OpenMapTiles|Elastic Maps Service

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

© OpenStreetMap contributors|OpenMapTiles|Elastic Maps Service

\"}},\"selectedLayer\":{\"name\":\"World Countries\",\"origin\":\"elastic_maps_service\",\"id\":\"world_countries\",\"created_at\":\"2017-04-26T17:12:15.978370\",\"attribution\":\"Made with NaturalEarth | Elastic Maps Service\",\"fields\":[{\"type\":\"id\",\"name\":\"iso2\",\"description\":\"ISO 3166-1 alpha-2 code\"},{\"type\":\"id\",\"name\":\"iso3\",\"description\":\"ISO 3166-1 alpha-3 code\"},{\"type\":\"property\",\"name\":\"name\",\"description\":\"name\"}],\"format\":{\"type\":\"geojson\"},\"layerId\":\"elastic_maps_service.World Countries\",\"isEMS\":true}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.dest\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.5","id":"25bdc750-6242-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,37],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzM5LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_verticalbarchart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_verticalbarchart\",\"type\":\"histogram\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\",\"defaultYExtents\":true},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"showCircles\":true}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":true,\"row\":true,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2015-09-18T06:38:43.311Z\",\"to\":\"2015-09-26T04:02:51.104Z\",\"mode\":\"absolute\"},\"useNormalizedEsInterval\":true,\"interval\":\"h\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{},\"scaleMetricValues\":true}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"split\",\"params\":{\"field\":\"response.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Response code\"}}]}"},"coreMigrationVersion":"7.13.5","id":"71dd7bc0-6248-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,39],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzQwLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_metricviz","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_metricviz\",\"type\":\"metric\",\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"range\",\"schema\":\"group\",\"params\":{\"field\":\"bytes_scripted\",\"ranges\":[{\"from\":0,\"to\":10000},{\"from\":10001,\"to\":300000}]}}]}"},"coreMigrationVersion":"7.13.5","id":"6aea48a0-6240-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,41],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzQxLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_piechart","uiStateJSON":"{}","version":1,"visState":"{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"field\":\"machine.os.raw\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"size\":5},\"schema\":\"segment\",\"type\":\"terms\"}],\"params\":{\"addLegend\":true,\"addTooltip\":true,\"isDonut\":true,\"labels\":{\"last_level\":true,\"show\":false,\"truncate\":100,\"values\":true},\"legendPosition\":\"right\",\"type\":\"pie\"},\"title\":\"logstash_piechart\",\"type\":\"pie\"}"},"coreMigrationVersion":"7.13.5","id":"32b681f0-6241-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,43],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzQyLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"logstash_tagcloud","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_tagcloud\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"log\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.srcdest\",\"size\":23,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.5","id":"ccca99e0-6244-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,45],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzQzLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"title":"logstash_timelion","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_timelion\",\"type\":\"timelion\",\"params\":{\"expression\":\".es(q='machine.os.raw:win xp' , index=logstash-*)\",\"interval\":\"auto\"},\"aggs\":[]}"},"coreMigrationVersion":"7.13.5","id":"a4d7be80-6245-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[],"sort":[1651599812857,46],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzQ0LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{}"},"title":"logstash_tsvb","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_tsvb\",\"type\":\"metrics\",\"params\":{\"id\":\"61ca57f0-469d-11e7-af02-69e470af7417\",\"type\":\"timeseries\",\"series\":[{\"id\":\"61ca57f1-469d-11e7-af02-69e470af7417\",\"color\":\"#68BC00\",\"split_mode\":\"everything\",\"metrics\":[{\"id\":\"61ca57f2-469d-11e7-af02-69e470af7417\",\"type\":\"count\"}],\"separate_axis\":0,\"axis_position\":\"right\",\"formatter\":\"number\",\"chart_type\":\"line\",\"line_width\":1,\"point_size\":1,\"fill\":0.5,\"stacked\":\"none\",\"split_color_mode\":\"gradient\"}],\"time_field\":\"@timestamp\",\"index_pattern\":\"\",\"interval\":\"auto\",\"axis_position\":\"left\",\"axis_formatter\":\"number\",\"axis_scale\":\"normal\",\"show_legend\":1,\"show_grid\":1,\"annotations\":[{\"fields\":\"machine.os.raw\",\"template\":\"{{machine.os.raw}}\",\"index_pattern\":\"logstash-*\",\"query_string\":{\"query\":\"machine.os.raw :\\\"win xp\\\" \",\"language\":\"lucene\"},\"id\":\"aa43ceb0-6248-11eb-9a82-ef1c6e6c0265\",\"color\":\"#F00\",\"time_field\":\"@timestamp\",\"icon\":\"fa-tag\",\"ignore_global_filters\":1,\"ignore_panel_filters\":1}],\"use_kibana_indexes\":false},\"aggs\":[]}"},"coreMigrationVersion":"7.13.5","id":"c94d8440-6248-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[],"sort":[1651599812857,47],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzQ1LDFd"} +{"attributes":{"columns":["bytes_scripted"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"machine.os.raw :\\\"win xp\\\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["@timestamp","desc"]],"title":"logstash_scripted_saved_search","version":1},"coreMigrationVersion":"7.13.5","id":"db6226f0-61c0-11eb-aebf-c306684b328d","migrationVersion":{"search":"7.9.3"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,49],"type":"search","updated_at":"2022-05-03T17:43:32.857Z","version":"WzQ2LDFd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{},\"vis\":null},\"panelRefName\":\"panel_2\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"3\",\"w\":24,\"x\":0,\"y\":15},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"4\",\"w\":24,\"x\":24,\"y\":15},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"5\",\"w\":24,\"x\":0,\"y\":30},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{},\"vis\":null},\"panelRefName\":\"panel_5\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"6\",\"w\":24,\"x\":24,\"y\":30},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{},\"vis\":null},\"panelRefName\":\"panel_6\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"7\",\"w\":24,\"x\":0,\"y\":45},\"panelIndex\":\"7\",\"embeddableConfig\":{\"enhancements\":{},\"vis\":null},\"panelRefName\":\"panel_7\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"8\",\"w\":24,\"x\":24,\"y\":45},\"panelIndex\":\"8\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"9\",\"w\":24,\"x\":0,\"y\":60},\"panelIndex\":\"9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"10\",\"w\":24,\"x\":24,\"y\":60},\"panelIndex\":\"10\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_10\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"11\",\"w\":24,\"x\":0,\"y\":75},\"panelIndex\":\"11\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_11\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"12\",\"w\":24,\"x\":24,\"y\":75},\"panelIndex\":\"12\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_12\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"13\",\"w\":24,\"x\":0,\"y\":90},\"panelIndex\":\"13\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"14\",\"w\":24,\"x\":24,\"y\":90},\"panelIndex\":\"14\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_14\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"15\",\"w\":24,\"x\":0,\"y\":105},\"panelIndex\":\"15\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_15\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"16\",\"w\":24,\"x\":24,\"y\":105},\"panelIndex\":\"16\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_16\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"17\",\"w\":24,\"x\":0,\"y\":120},\"panelIndex\":\"17\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_17\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"18\",\"w\":24,\"x\":24,\"y\":120},\"panelIndex\":\"18\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_18\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"19\",\"w\":24,\"x\":0,\"y\":135},\"panelIndex\":\"19\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_19\"},{\"version\":\"7.13.5\",\"type\":\"search\",\"gridData\":{\"h\":15,\"i\":\"20\",\"w\":24,\"x\":24,\"y\":135},\"panelIndex\":\"20\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_20\"}]","refreshInterval":{"pause":true,"value":0},"timeFrom":"2015-09-20T01:56:56.132Z","timeRestore":true,"timeTo":"2015-09-21T11:18:20.471Z","title":"logstash_dashboardwithtime","version":1},"coreMigrationVersion":"7.13.5","id":"154944b0-6249-11eb-aebf-c306684b328d","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"36b91810-6239-11eb-aebf-c306684b328d","name":"1:panel_1","type":"visualization"},{"id":"0a274320-61cc-11eb-aebf-c306684b328d","name":"2:panel_2","type":"visualization"},{"id":"e4aef350-623d-11eb-aebf-c306684b328d","name":"3:panel_3","type":"visualization"},{"id":"f92e5630-623e-11eb-aebf-c306684b328d","name":"4:panel_4","type":"visualization"},{"id":"9853d4d0-623d-11eb-aebf-c306684b328d","name":"5:panel_5","type":"visualization"},{"id":"6ecb33b0-623d-11eb-aebf-c306684b328d","name":"6:panel_6","type":"visualization"},{"id":"b8e35c80-623c-11eb-aebf-c306684b328d","name":"7:panel_7","type":"visualization"},{"id":"f1bc75d0-6239-11eb-aebf-c306684b328d","name":"8:panel_8","type":"visualization"},{"id":"0d8a8860-623a-11eb-aebf-c306684b328d","name":"9:panel_9","type":"visualization"},{"id":"d79fe3d0-6239-11eb-aebf-c306684b328d","name":"10:panel_10","type":"visualization"},{"id":"318375a0-6240-11eb-aebf-c306684b328d","name":"11:panel_11","type":"visualization"},{"id":"e461eb20-6245-11eb-aebf-c306684b328d","name":"12:panel_12","type":"visualization"},{"id":"25bdc750-6242-11eb-aebf-c306684b328d","name":"13:panel_13","type":"visualization"},{"id":"71dd7bc0-6248-11eb-aebf-c306684b328d","name":"14:panel_14","type":"visualization"},{"id":"6aea48a0-6240-11eb-aebf-c306684b328d","name":"15:panel_15","type":"visualization"},{"id":"32b681f0-6241-11eb-aebf-c306684b328d","name":"16:panel_16","type":"visualization"},{"id":"ccca99e0-6244-11eb-aebf-c306684b328d","name":"17:panel_17","type":"visualization"},{"id":"a4d7be80-6245-11eb-aebf-c306684b328d","name":"18:panel_18","type":"visualization"},{"id":"c94d8440-6248-11eb-aebf-c306684b328d","name":"19:panel_19","type":"visualization"},{"id":"db6226f0-61c0-11eb-aebf-c306684b328d","name":"20:panel_20","type":"search"}],"sort":[1651613667249,1638],"type":"dashboard","updated_at":"2022-05-03T21:34:27.249Z","version":"WzExNDMsMV0="} +{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"26e2cf99-d931-4320-9e15-9dbc148f3534":{"columnOrder":["6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e","beb72af1-239c-46d8-823b-b00d1e2ace43"],"columns":{"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e":{"dataType":"string","isBucketed":true,"label":"Top values of url.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"beb72af1-239c-46d8-823b-b00d1e2ace43","type":"column"},"orderDirection":"desc","otherBucket":true,"size":20},"scale":"ordinal","sourceField":"url.raw"},"beb72af1-239c-46d8-823b-b00d1e2ace43":{"dataType":"number","isBucketed":false,"label":"Unique count of geo.srcdest","operationType":"unique_count","scale":"ratio","sourceField":"geo.srcdest"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"layers":[{"categoryDisplay":"default","groups":["6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e","6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e","6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e"],"layerId":"26e2cf99-d931-4320-9e15-9dbc148f3534","legendDisplay":"default","metric":"beb72af1-239c-46d8-823b-b00d1e2ace43","nestedLegend":false,"numberDisplay":"percent"}],"shape":"donut"}},"title":"lens_pie_chart","visualizationType":"lnsPie"},"coreMigrationVersion":"7.13.5","id":"21905950-bd9f-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-26e2cf99-d931-4320-9e15-9dbc148f3534","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,53],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"WzQ4LDFd"} +{"attributes":{"description":null,"state":{"datasourceStates":{"indexpattern":{"layers":{"a3ac0e3d-63ec-49b2-882a-b34680a967ba":{"columnOrder":["352a2c02-aa6f-4a35-b776-45c3715a6c5e","8ef68cbb-e039-49d6-b15e-be81559f4b55","14fad6b1-6a7c-4ae8-ae4b-d9569e31e04a"],"columns":{"14fad6b1-6a7c-4ae8-ae4b-d9569e31e04a":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"352a2c02-aa6f-4a35-b776-45c3715a6c5e":{"dataType":"string","isBucketed":true,"label":"Top values of geo.srcdest","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"14fad6b1-6a7c-4ae8-ae4b-d9569e31e04a","type":"column"},"orderDirection":"desc","otherBucket":true,"size":67},"scale":"ordinal","sourceField":"geo.srcdest"},"8ef68cbb-e039-49d6-b15e-be81559f4b55":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["14fad6b1-6a7c-4ae8-ae4b-d9569e31e04a"],"layerId":"a3ac0e3d-63ec-49b2-882a-b34680a967ba","position":"top","seriesType":"bar_percentage_stacked","showGridlines":false,"splitAccessor":"352a2c02-aa6f-4a35-b776-45c3715a6c5e","xAccessor":"8ef68cbb-e039-49d6-b15e-be81559f4b55"}],"legend":{"isVisible":true,"position":"top","showSingleSeries":true},"preferredSeriesType":"bar_percentage_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_bar_verticalpercentage","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.5","id":"aa4b8da0-bd9f-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-a3ac0e3d-63ec-49b2-882a-b34680a967ba","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,57],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"WzQ5LDFd"} +{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"037b7937-790b-4d2d-94a5-7f5837a6ef05":{"columnOrder":["b3d46616-75e0-419e-97ea-91148961ef94","025a0fb3-dc44-4f5c-b517-2d71d3f26f14","c476db14-0cc1-40ec-863e-d2779256a407"],"columns":{"025a0fb3-dc44-4f5c-b517-2d71d3f26f14":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"b3d46616-75e0-419e-97ea-91148961ef94":{"dataType":"string","isBucketed":true,"label":"Top values of geo.srcdest","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"c476db14-0cc1-40ec-863e-d2779256a407","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"geo.srcdest"},"c476db14-0cc1-40ec-863e-d2779256a407":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"lucene","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["c476db14-0cc1-40ec-863e-d2779256a407"],"layerId":"037b7937-790b-4d2d-94a5-7f5837a6ef05","position":"top","seriesType":"bar_stacked","showGridlines":false,"splitAccessor":"b3d46616-75e0-419e-97ea-91148961ef94","xAccessor":"025a0fb3-dc44-4f5c-b517-2d71d3f26f14"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_barchart_vertical","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.5","id":"2d3f1250-bd9f-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-037b7937-790b-4d2d-94a5-7f5837a6ef05","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,61],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"WzUwLDFd"} +{"attributes":{"description":null,"state":{"datasourceStates":{"indexpattern":{"layers":{"212688dc-e7d7-4875-a221-09e6191bdcf7":{"columnOrder":["05410186-83c4-460a-82bf-dd7e9d998c9f","e8659feb-1db4-4706-9147-ac1fd513a1ba","c9a32fd0-a465-44fb-8adc-b957fb72cad5"],"columns":{"05410186-83c4-460a-82bf-dd7e9d998c9f":{"dataType":"string","isBucketed":true,"label":"Top values of extension.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"c9a32fd0-a465-44fb-8adc-b957fb72cad5","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"extension.raw"},"c9a32fd0-a465-44fb-8adc-b957fb72cad5":{"dataType":"number","isBucketed":false,"label":"Average of bytes","operationType":"average","scale":"ratio","sourceField":"bytes"},"e8659feb-1db4-4706-9147-ac1fd513a1ba":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["c9a32fd0-a465-44fb-8adc-b957fb72cad5"],"layerId":"212688dc-e7d7-4875-a221-09e6191bdcf7","position":"top","seriesType":"bar_horizontal_stacked","showGridlines":false,"splitAccessor":"05410186-83c4-460a-82bf-dd7e9d998c9f","xAccessor":"e8659feb-1db4-4706-9147-ac1fd513a1ba"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_horizontal_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_barhorizontal_stacked","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.5","id":"edd5a560-bda4-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-212688dc-e7d7-4875-a221-09e6191bdcf7","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,65],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"WzUxLDFd"} +{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"7ab04fd4-04da-4023-8899-d94620193607":{"columnOrder":["0ab2d5f8-11f0-4b25-b8bb-3127a3b8d4c7","9eb851dd-31f6-481a-84d1-9ecce53a6ad2","f6b271a7-509b-4c37-b7b6-ac5be4bcb49a"],"columns":{"0ab2d5f8-11f0-4b25-b8bb-3127a3b8d4c7":{"dataType":"string","isBucketed":true,"label":"Top values of request.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"f6b271a7-509b-4c37-b7b6-ac5be4bcb49a","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"request.raw"},"9eb851dd-31f6-481a-84d1-9ecce53a6ad2":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"f6b271a7-509b-4c37-b7b6-ac5be4bcb49a":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["f6b271a7-509b-4c37-b7b6-ac5be4bcb49a"],"layerId":"7ab04fd4-04da-4023-8899-d94620193607","position":"top","seriesType":"bar_horizontal_percentage_stacked","showGridlines":false,"splitAccessor":"0ab2d5f8-11f0-4b25-b8bb-3127a3b8d4c7","xAccessor":"9eb851dd-31f6-481a-84d1-9ecce53a6ad2"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_horizontal_percentage_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_barhorizontalpercentage","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.5","id":"2c25a450-bda5-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-7ab04fd4-04da-4023-8899-d94620193607","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,69],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"WzUyLDFd"} +{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"037b7937-790b-4d2d-94a5-7f5837a6ef05":{"columnOrder":["b3d46616-75e0-419e-97ea-91148961ef94","025a0fb3-dc44-4f5c-b517-2d71d3f26f14","c476db14-0cc1-40ec-863e-d2779256a407"],"columns":{"025a0fb3-dc44-4f5c-b517-2d71d3f26f14":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"b3d46616-75e0-419e-97ea-91148961ef94":{"dataType":"string","isBucketed":true,"label":"Top values of geo.srcdest","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"c476db14-0cc1-40ec-863e-d2779256a407","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"geo.srcdest"},"c476db14-0cc1-40ec-863e-d2779256a407":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"lucene","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["c476db14-0cc1-40ec-863e-d2779256a407"],"layerId":"037b7937-790b-4d2d-94a5-7f5837a6ef05","position":"top","seriesType":"bar_stacked","showGridlines":false,"splitAccessor":"b3d46616-75e0-419e-97ea-91148961ef94","xAccessor":"025a0fb3-dc44-4f5c-b517-2d71d3f26f14"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_visualization","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.5","id":"e79116e0-bd9e-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-037b7937-790b-4d2d-94a5-7f5837a6ef05","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651612765795,977],"type":"lens","updated_at":"2022-05-03T21:19:25.795Z","version":"WzU1NSwxXQ=="} +{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"72783e5f-aa7b-4b8a-b26c-a3e4d051340e":{"columnOrder":["0f619652-9ff1-453b-ae1f-7371baa82f55"],"columns":{"0f619652-9ff1-453b-ae1f-7371baa82f55":{"dataType":"number","isBucketed":false,"label":"Average of phpmemory","operationType":"average","params":{"format":{"id":"percent","params":{"decimals":10}}},"scale":"ratio","sourceField":"phpmemory"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"accessor":"0f619652-9ff1-453b-ae1f-7371baa82f55","layerId":"72783e5f-aa7b-4b8a-b26c-a3e4d051340e"}},"title":"lens_metric","visualizationType":"lnsMetric"},"coreMigrationVersion":"7.13.5","id":"974fb950-bda5-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-72783e5f-aa7b-4b8a-b26c-a3e4d051340e","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,73],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"WzU0LDFd"} +{"attributes":{"description":null,"state":{"datasourceStates":{"indexpattern":{"layers":{"bb478774-f9e8-4380-bf3a-f4a89a4d79b5":{"columnOrder":["4573ae8f-8f9d-4918-b496-c08f7102c6e1","cebdc6c5-3587-4f57-879c-dd63ea99cf03"],"columns":{"4573ae8f-8f9d-4918-b496-c08f7102c6e1":{"dataType":"string","isBucketed":true,"label":"Top values of machine.os.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"cebdc6c5-3587-4f57-879c-dd63ea99cf03","type":"column"},"orderDirection":"desc","otherBucket":true,"size":5},"scale":"ordinal","sourceField":"machine.os.raw"},"cebdc6c5-3587-4f57-879c-dd63ea99cf03":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"layers":[{"categoryDisplay":"default","groups":["4573ae8f-8f9d-4918-b496-c08f7102c6e1"],"layerId":"bb478774-f9e8-4380-bf3a-f4a89a4d79b5","legendDisplay":"default","metric":"cebdc6c5-3587-4f57-879c-dd63ea99cf03","nestedLegend":false,"numberDisplay":"percent"}],"shape":"pie"}},"title":"lens_piechart","visualizationType":"lnsPie"},"coreMigrationVersion":"7.13.5","id":"51b63040-bda5-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-bb478774-f9e8-4380-bf3a-f4a89a4d79b5","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,77],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"WzU1LDFd"} +{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"a1b85651-db29-441f-8f08-cf1b9b6f7bf1":{"columnOrder":["2b3bdc32-0be0-49dc-993d-4630b0bd1185","b85cc0a7-0b18-4b08-b7f0-c617f80cf903","03203126-8286-444d-b5b3-4f399eaf2c26","44305317-61e8-4600-9f3c-ac4070e0c529"],"columns":{"03203126-8286-444d-b5b3-4f399eaf2c26":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"2b3bdc32-0be0-49dc-993d-4630b0bd1185":{"dataType":"string","isBucketed":true,"label":"Top values of extension.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"44305317-61e8-4600-9f3c-ac4070e0c529","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"extension.raw"},"44305317-61e8-4600-9f3c-ac4070e0c529":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"b85cc0a7-0b18-4b08-b7f0-c617f80cf903":{"dataType":"string","isBucketed":true,"label":"Top values of machine.os.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"44305317-61e8-4600-9f3c-ac4070e0c529","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"machine.os.raw"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"columns":[{"columnId":"2b3bdc32-0be0-49dc-993d-4630b0bd1185","isTransposed":false},{"columnId":"b85cc0a7-0b18-4b08-b7f0-c617f80cf903","isTransposed":false},{"columnId":"03203126-8286-444d-b5b3-4f399eaf2c26","isTransposed":false},{"columnId":"44305317-61e8-4600-9f3c-ac4070e0c529","isTransposed":false}],"layerId":"a1b85651-db29-441f-8f08-cf1b9b6f7bf1"}},"title":"lens_table","visualizationType":"lnsDatatable"},"coreMigrationVersion":"7.13.5","id":"b00679c0-bda5-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-a1b85651-db29-441f-8f08-cf1b9b6f7bf1","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,81],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"WzU2LDFd"} +{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"4fbb51e9-1f99-4b5e-b59d-60fcb547b1d9":{"columnOrder":["08a1af05-743d-480e-9056-3405b1bdda7d","bae35990-75c2-487f-94eb-d8e03d2eda33"],"columns":{"08a1af05-743d-480e-9056-3405b1bdda7d":{"dataType":"string","isBucketed":true,"label":"Top values of geo.srcdest","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"bae35990-75c2-487f-94eb-d8e03d2eda33","type":"column"},"orderDirection":"desc","otherBucket":true,"size":25},"scale":"ordinal","sourceField":"geo.srcdest"},"bae35990-75c2-487f-94eb-d8e03d2eda33":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"layers":[{"categoryDisplay":"default","groups":["08a1af05-743d-480e-9056-3405b1bdda7d","08a1af05-743d-480e-9056-3405b1bdda7d","08a1af05-743d-480e-9056-3405b1bdda7d"],"layerId":"4fbb51e9-1f99-4b5e-b59d-60fcb547b1d9","legendDisplay":"default","metric":"bae35990-75c2-487f-94eb-d8e03d2eda33","nestedLegend":false,"numberDisplay":"percent"}],"shape":"treemap"}},"title":"lens_treemap","visualizationType":"lnsPie"},"coreMigrationVersion":"7.13.5","id":"652ade10-bd9f-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-4fbb51e9-1f99-4b5e-b59d-60fcb547b1d9","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,85],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"WzU3LDFd"} +{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"e84503c1-4dbd-4ac6-9ac9-ad938654680f":{"columnOrder":["38c73fd4-6330-4162-8a7b-1a059f005da8","e8d4dad2-ac30-4741-aca0-904eb1fc8455","70433aa7-3c2c-4e6c-b8cf-4218c995cff5"],"columns":{"38c73fd4-6330-4162-8a7b-1a059f005da8":{"dataType":"string","isBucketed":true,"label":"Top values of url.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"70433aa7-3c2c-4e6c-b8cf-4218c995cff5","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"url.raw"},"70433aa7-3c2c-4e6c-b8cf-4218c995cff5":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"e8d4dad2-ac30-4741-aca0-904eb1fc8455":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["70433aa7-3c2c-4e6c-b8cf-4218c995cff5"],"layerId":"e84503c1-4dbd-4ac6-9ac9-ad938654680f","position":"top","seriesType":"line","showGridlines":false,"splitAccessor":"38c73fd4-6330-4162-8a7b-1a059f005da8","xAccessor":"e8d4dad2-ac30-4741-aca0-904eb1fc8455"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"line","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_line_chart","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.5","id":"7f3b5fb0-be2f-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-e84503c1-4dbd-4ac6-9ac9-ad938654680f","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,89],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"WzU4LDFd"} +{"attributes":{"fieldAttrs":"{\"speaker\":{\"count\":1},\"text_entry\":{\"count\":6},\"type\":{\"count\":3}}","fields":"[]","runtimeFieldMap":"{}","title":"shakespeare"},"coreMigrationVersion":"7.13.5","id":"4e937b20-619d-11eb-aebf-c306684b328d","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"sort":[1651599812857,90],"type":"index-pattern","updated_at":"2022-05-03T17:43:32.857Z","version":"WzU5LDFd"} +{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"d35680ce-c285-4fae-89d6-1245671bbc78":{"columnOrder":["2bcbffbe-c24d-4e74-8a03-9a6da7db70c0","6b00fde6-bfaa-4da1-beeb-bfd85a4cb2ff","8319857d-a03b-4158-bdf1-2a788e510445"],"columns":{"2bcbffbe-c24d-4e74-8a03-9a6da7db70c0":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"6b00fde6-bfaa-4da1-beeb-bfd85a4cb2ff":{"dataType":"number","isBucketed":false,"label":"Average of bytes","operationType":"average","scale":"ratio","sourceField":"bytes"},"8319857d-a03b-4158-bdf1-2a788e510445":{"dataType":"number","isBucketed":false,"label":"Sum of bytes_scripted","operationType":"sum","params":{"format":{"id":"number","params":{"decimals":2}}},"scale":"ratio","sourceField":"bytes_scripted"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["6b00fde6-bfaa-4da1-beeb-bfd85a4cb2ff","8319857d-a03b-4158-bdf1-2a788e510445"],"layerId":"d35680ce-c285-4fae-89d6-1245671bbc78","position":"top","seriesType":"area","showGridlines":false,"xAccessor":"2bcbffbe-c24d-4e74-8a03-9a6da7db70c0","yConfig":[{"axisMode":"auto","forAccessor":"8319857d-a03b-4158-bdf1-2a788e510445"}]}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"area","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_area_chart","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.5","id":"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-d35680ce-c285-4fae-89d6-1245671bbc78","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,94],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"WzYwLDFd"} +{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"70bd567e-8e67-4696-a406-313b06344fa9":{"columnOrder":["96ddedfb-043b-479e-a746-600e72ab546e","d325b7da-4266-4035-9b13-5f853615149a","2fc1391b-17d1-4c49-9ddc-06ff307e3520","1cc6f19c-cbcb-4abd-b56d-1a2f9deae5f3"],"columns":{"1cc6f19c-cbcb-4abd-b56d-1a2f9deae5f3":{"dataType":"number","isBucketed":false,"label":"Average of machine.ram","operationType":"average","scale":"ratio","sourceField":"machine.ram"},"2fc1391b-17d1-4c49-9ddc-06ff307e3520":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"96ddedfb-043b-479e-a746-600e72ab546e":{"dataType":"string","isBucketed":true,"label":"Top values of machine.os.raw","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"2fc1391b-17d1-4c49-9ddc-06ff307e3520","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"machine.os.raw"},"d325b7da-4266-4035-9b13-5f853615149a":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["2fc1391b-17d1-4c49-9ddc-06ff307e3520","1cc6f19c-cbcb-4abd-b56d-1a2f9deae5f3"],"layerId":"70bd567e-8e67-4696-a406-313b06344fa9","position":"top","seriesType":"area_stacked","showGridlines":false,"splitAccessor":"96ddedfb-043b-479e-a746-600e72ab546e","xAccessor":"d325b7da-4266-4035-9b13-5f853615149a"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"area_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_area_stacked","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.5","id":"dd315430-be2f-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-70bd567e-8e67-4696-a406-313b06344fa9","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,98],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"WzYxLDFd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"2e80716f-c1b6-46f2-be2b-35db744b5031\"},\"panelIndex\":\"2e80716f-c1b6-46f2-be2b-35db744b5031\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsPie\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"26e2cf99-d931-4320-9e15-9dbc148f3534\":{\"columns\":{\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\":{\"label\":\"Top values of url.raw\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"url.raw\",\"isBucketed\":true,\"params\":{\"size\":20,\"orderBy\":{\"type\":\"column\",\"columnId\":\"beb72af1-239c-46d8-823b-b00d1e2ace43\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"beb72af1-239c-46d8-823b-b00d1e2ace43\":{\"label\":\"Unique count of geo.srcdest\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"geo.srcdest\",\"isBucketed\":false}},\"columnOrder\":[\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"beb72af1-239c-46d8-823b-b00d1e2ace43\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"shape\":\"donut\",\"layers\":[{\"layerId\":\"26e2cf99-d931-4320-9e15-9dbc148f3534\",\"groups\":[\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\"],\"metric\":\"beb72af1-239c-46d8-823b-b00d1e2ace43\",\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"default\",\"nestedLegend\":false}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-26e2cf99-d931-4320-9e15-9dbc148f3534\"}]},\"enhancements\":{},\"type\":\"lens\"},\"panelRefName\":\"panel_2e80716f-c1b6-46f2-be2b-35db744b5031\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"da8843e0-6789-4aae-bcd0-81f270538719\"},\"panelIndex\":\"da8843e0-6789-4aae-bcd0-81f270538719\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_da8843e0-6789-4aae-bcd0-81f270538719\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"adcd4418-7299-4efa-b369-5f71a7b4ebe0\"},\"panelIndex\":\"adcd4418-7299-4efa-b369-5f71a7b4ebe0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_adcd4418-7299-4efa-b369-5f71a7b4ebe0\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"869754a7-edf0-478f-a7f1-80374f63108a\"},\"panelIndex\":\"869754a7-edf0-478f-a7f1-80374f63108a\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_869754a7-edf0-478f-a7f1-80374f63108a\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":30,\"w\":24,\"h\":15,\"i\":\"67111cf4-338e-453f-8621-e8dea64082d1\"},\"panelIndex\":\"67111cf4-338e-453f-8621-e8dea64082d1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_67111cf4-338e-453f-8621-e8dea64082d1\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\"},\"panelIndex\":\"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":45,\"w\":24,\"h\":15,\"i\":\"88847944-ae1b-45fd-b102-3b45f9bea04b\"},\"panelIndex\":\"88847944-ae1b-45fd-b102-3b45f9bea04b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_88847944-ae1b-45fd-b102-3b45f9bea04b\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":45,\"w\":24,\"h\":15,\"i\":\"5a7924c7-eac0-4573-9199-fecec5b82e9e\"},\"panelIndex\":\"5a7924c7-eac0-4573-9199-fecec5b82e9e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5a7924c7-eac0-4573-9199-fecec5b82e9e\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":60,\"w\":24,\"h\":15,\"i\":\"f8f49591-f071-4a96-b1ed-cd65daff5648\"},\"panelIndex\":\"f8f49591-f071-4a96-b1ed-cd65daff5648\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_f8f49591-f071-4a96-b1ed-cd65daff5648\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":60,\"w\":24,\"h\":15,\"i\":\"9f357f47-c2a0-421f-a456-9583c40837ab\"},\"panelIndex\":\"9f357f47-c2a0-421f-a456-9583c40837ab\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9f357f47-c2a0-421f-a456-9583c40837ab\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":75,\"w\":24,\"h\":15,\"i\":\"6cb383e9-1e80-44f9-80d5-7b8c585668db\"},\"panelIndex\":\"6cb383e9-1e80-44f9-80d5-7b8c585668db\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6cb383e9-1e80-44f9-80d5-7b8c585668db\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":75,\"w\":24,\"h\":15,\"i\":\"57f5f0bf-6610-4599-aad4-37484640b5e2\"},\"panelIndex\":\"57f5f0bf-6610-4599-aad4-37484640b5e2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_57f5f0bf-6610-4599-aad4-37484640b5e2\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":90,\"w\":24,\"h\":15,\"i\":\"32d3ab66-52e1-44e3-8c1f-1dccff3c5692\"},\"panelIndex\":\"32d3ab66-52e1-44e3-8c1f-1dccff3c5692\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_32d3ab66-52e1-44e3-8c1f-1dccff3c5692\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":90,\"w\":24,\"h\":15,\"i\":\"dd1718fd-74ee-4032-851b-db97e893825d\"},\"panelIndex\":\"dd1718fd-74ee-4032-851b-db97e893825d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_dd1718fd-74ee-4032-851b-db97e893825d\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":105,\"w\":24,\"h\":15,\"i\":\"98a556ee-078b-4e03-93a8-29996133cdcb\"},\"panelIndex\":\"98a556ee-078b-4e03-93a8-29996133cdcb\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"999a2d60-cb2a-451c-8d71-80d7e92e70fd\":{\"columns\":{\"ce9117a2-773c-474c-8fb1-18940cf58b38\":{\"label\":\"Top values of type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"type\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\"},\"orderDirection\":\"asc\",\"otherBucket\":true,\"missingBucket\":false}},\"a3d10552-e352-40d0-a156-e86112c0501a\":{\"label\":\"Top values of _type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"_type\",\"isBucketed\":true,\"params\":{\"size\":3,\"orderBy\":{\"type\":\"column\",\"columnId\":\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"},\"9c5db2f3-9eb0-4667-9a74-3318301de251\":{\"label\":\"Sum of bytes\",\"dataType\":\"number\",\"operationType\":\"sum\",\"sourceField\":\"bytes\",\"isBucketed\":false,\"scale\":\"ratio\"}},\"columnOrder\":[\"ce9117a2-773c-474c-8fb1-18940cf58b38\",\"a3d10552-e352-40d0-a156-e86112c0501a\",\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\",\"9c5db2f3-9eb0-4667-9a74-3318301de251\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"999a2d60-cb2a-451c-8d71-80d7e92e70fd\",\"accessors\":[\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\",\"9c5db2f3-9eb0-4667-9a74-3318301de251\"],\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"xAccessor\":\"ce9117a2-773c-474c-8fb1-18940cf58b38\",\"splitAccessor\":\"a3d10552-e352-40d0-a156-e86112c0501a\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-999a2d60-cb2a-451c-8d71-80d7e92e70fd\"}]},\"enhancements\":{},\"type\":\"lens\"}},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":105,\"w\":24,\"h\":15,\"i\":\"62a0f0b0-3589-4cef-807b-b1b4258b7a9b\"},\"panelIndex\":\"62a0f0b0-3589-4cef-807b-b1b4258b7a9b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_62a0f0b0-3589-4cef-807b-b1b4258b7a9b\"},{\"version\":\"7.13.1\",\"type\":\"map\",\"gridData\":{\"x\":0,\"y\":120,\"w\":24,\"h\":15,\"i\":\"dcc0defa-3376-465c-9b5b-2ba69528848c\"},\"panelIndex\":\"dcc0defa-3376-465c-9b5b-2ba69528848c\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":19.94277,\"lon\":0,\"zoom\":1.56},\"mapBuffer\":{\"minLon\":-210.32666,\"minLat\":-64.8435,\"maxLon\":210.32666,\"maxLat\":95.13806},\"isLayerTOCOpen\":true,\"openTOCDetails\":[],\"hiddenLayers\":[],\"enhancements\":{}},\"panelRefName\":\"panel_dcc0defa-3376-465c-9b5b-2ba69528848c\"}]","refreshInterval":{"pause":true,"value":0},"timeFrom":"2015-09-20T01:56:56.132Z","timeRestore":true,"timeTo":"2015-09-21T11:18:20.471Z","title":"lens_maps_dashboard_logstash","version":1},"coreMigrationVersion":"7.13.5","id":"16d86080-be5c-11eb-9520-1b4c3ca6a781","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"21905950-bd9f-11eb-9520-1b4c3ca6a781","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:panel_2e80716f-c1b6-46f2-be2b-35db744b5031","type":"lens"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:indexpattern-datasource-layer-26e2cf99-d931-4320-9e15-9dbc148f3534","type":"index-pattern"},{"id":"aa4b8da0-bd9f-11eb-9520-1b4c3ca6a781","name":"da8843e0-6789-4aae-bcd0-81f270538719:panel_da8843e0-6789-4aae-bcd0-81f270538719","type":"lens"},{"id":"2d3f1250-bd9f-11eb-9520-1b4c3ca6a781","name":"adcd4418-7299-4efa-b369-5f71a7b4ebe0:panel_adcd4418-7299-4efa-b369-5f71a7b4ebe0","type":"lens"},{"id":"edd5a560-bda4-11eb-9520-1b4c3ca6a781","name":"869754a7-edf0-478f-a7f1-80374f63108a:panel_869754a7-edf0-478f-a7f1-80374f63108a","type":"lens"},{"id":"2c25a450-bda5-11eb-9520-1b4c3ca6a781","name":"67111cf4-338e-453f-8621-e8dea64082d1:panel_67111cf4-338e-453f-8621-e8dea64082d1","type":"lens"},{"id":"e79116e0-bd9e-11eb-9520-1b4c3ca6a781","name":"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d:panel_13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d","type":"lens"},{"id":"974fb950-bda5-11eb-9520-1b4c3ca6a781","name":"88847944-ae1b-45fd-b102-3b45f9bea04b:panel_88847944-ae1b-45fd-b102-3b45f9bea04b","type":"lens"},{"id":"21905950-bd9f-11eb-9520-1b4c3ca6a781","name":"5a7924c7-eac0-4573-9199-fecec5b82e9e:panel_5a7924c7-eac0-4573-9199-fecec5b82e9e","type":"lens"},{"id":"51b63040-bda5-11eb-9520-1b4c3ca6a781","name":"f8f49591-f071-4a96-b1ed-cd65daff5648:panel_f8f49591-f071-4a96-b1ed-cd65daff5648","type":"lens"},{"id":"b00679c0-bda5-11eb-9520-1b4c3ca6a781","name":"9f357f47-c2a0-421f-a456-9583c40837ab:panel_9f357f47-c2a0-421f-a456-9583c40837ab","type":"lens"},{"id":"652ade10-bd9f-11eb-9520-1b4c3ca6a781","name":"6cb383e9-1e80-44f9-80d5-7b8c585668db:panel_6cb383e9-1e80-44f9-80d5-7b8c585668db","type":"lens"},{"id":"7f3b5fb0-be2f-11eb-9520-1b4c3ca6a781","name":"57f5f0bf-6610-4599-aad4-37484640b5e2:panel_57f5f0bf-6610-4599-aad4-37484640b5e2","type":"lens"},{"id":"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781","name":"32d3ab66-52e1-44e3-8c1f-1dccff3c5692:panel_32d3ab66-52e1-44e3-8c1f-1dccff3c5692","type":"lens"},{"id":"dd315430-be2f-11eb-9520-1b4c3ca6a781","name":"dd1718fd-74ee-4032-851b-db97e893825d:panel_dd1718fd-74ee-4032-851b-db97e893825d","type":"lens"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"98a556ee-078b-4e03-93a8-29996133cdcb:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"98a556ee-078b-4e03-93a8-29996133cdcb:indexpattern-datasource-layer-999a2d60-cb2a-451c-8d71-80d7e92e70fd","type":"index-pattern"},{"id":"0dbbf8b0-be3c-11eb-9520-1b4c3ca6a781","name":"62a0f0b0-3589-4cef-807b-b1b4258b7a9b:panel_62a0f0b0-3589-4cef-807b-b1b4258b7a9b","type":"lens"},{"id":"0c5974f0-be5c-11eb-9520-1b4c3ca6a781","name":"dcc0defa-3376-465c-9b5b-2ba69528848c:panel_dcc0defa-3376-465c-9b5b-2ba69528848c","type":"map"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,120],"type":"dashboard","updated_at":"2022-05-03T17:43:32.857Z","version":"WzYyLDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"shakespeare_areachart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"shakespeare_areachart\",\"type\":\"histogram\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100,\"filter\":true},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"mode\":\"stacked\",\"type\":\"histogram\",\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"data\":{\"id\":\"2\",\"label\":\"Count\"},\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true},\"aggs\":[{\"id\":\"2\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"play_name\",\"size\":20,\"order\":\"desc\",\"orderBy\":\"2\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}},{\"id\":\"4\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"play_name\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"2\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.5","id":"185283c0-619e-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,122],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzY0LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"by_reference_logstash","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"by_reference_logstash\",\"type\":\"histogram\",\"params\":{\"type\":\"histogram\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{},\"style\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"},\"style\":{}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\",\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true}],\"radiusRatio\":0,\"addTooltip\":true,\"detailedTooltip\":true,\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"labels\":{\"show\":false},\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"2014-07-15T12:33:21.084Z\",\"to\":\"2019-01-28T03:18:12.440Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}},\"schema\":\"segment\"},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"response.raw\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"group\"}]}"},"coreMigrationVersion":"7.13.5","id":"1885abb0-ca2b-11eb-bf5e-3de94e83d4f0","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651612828183,981],"type":"visualization","updated_at":"2022-05-03T21:20:28.183Z","version":"WzU1OSwxXQ=="} +{"attributes":{"color":"#f44fcf","description":"","name":"shakespeare"},"coreMigrationVersion":"7.13.5","id":"42b4cec0-be32-11eb-9520-1b4c3ca6a781","references":[],"sort":[1651599812857,123],"type":"tag","updated_at":"2022-05-03T17:43:32.857Z","version":"WzY2LDFd"} +{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"3338dd55-4007-4be5-908d-25722b6174cb":{"columnOrder":["6c83b0c2-5834-4619-888c-9e8a08e47d42","b25e7497-c188-4c25-b002-1fd5bd69e76d"],"columns":{"6c83b0c2-5834-4619-888c-9e8a08e47d42":{"dataType":"string","isBucketed":true,"label":"Top values of speaker","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"b25e7497-c188-4c25-b002-1fd5bd69e76d","type":"column"},"orderDirection":"desc","otherBucket":false,"size":90},"scale":"ordinal","sourceField":"speaker"},"b25e7497-c188-4c25-b002-1fd5bd69e76d":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"layers":[{"categoryDisplay":"default","groups":["6c83b0c2-5834-4619-888c-9e8a08e47d42","6c83b0c2-5834-4619-888c-9e8a08e47d42","6c83b0c2-5834-4619-888c-9e8a08e47d42"],"layerId":"3338dd55-4007-4be5-908d-25722b6174cb","legendDisplay":"default","metric":"b25e7497-c188-4c25-b002-1fd5bd69e76d","nestedLegend":false,"numberDisplay":"percent"}],"palette":{"name":"complimentary","type":"palette"},"shape":"treemap"}},"title":"lens_shakespeare_treemap","visualizationType":"lnsPie"},"coreMigrationVersion":"7.13.5","id":"31e9f2f0-be32-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-3338dd55-4007-4be5-908d-25722b6174cb","type":"index-pattern"},{"id":"42b4cec0-be32-11eb-9520-1b4c3ca6a781","name":"tag-ref-42b4cec0-be32-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,127],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"WzY3LDFd"} +{"attributes":{"accessCount":0,"accessDate":1622059178542,"createDate":1622059178542,"url":"/app/dashboards#/view/73398a90-619e-11eb-aebf-c306684b328d?embed=true&_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:%272015-09-20T01:56:56.132Z%27,to:%272015-09-21T11:18:20.471Z%27))&_a=(description:%27%27,filters:!(),fullScreenMode:!f,options:(darkTheme:!f,hidePanelTitles:!f,useMargins:!t),panels:!((embeddableConfig:(enhancements:()),gridData:(h:15,i:%271%27,w:24,x:0,y:0),id:%27185283c0-619e-11eb-aebf-c306684b328d%27,panelIndex:%271%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%272%27,w:24,x:24,y:0),id:%2733736660-619e-11eb-aebf-c306684b328d%27,panelIndex:%272%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%273%27,w:24,x:0,y:15),id:%27622ac7f0-619e-11eb-aebf-c306684b328d%27,panelIndex:%273%27,type:visualization,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%274%27,w:24,x:24,y:15),id:%27712ebbe0-619d-11eb-aebf-c306684b328d%27,panelIndex:%274%27,type:search,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%275%27,w:24,x:0,y:30),id:ddacc820-619d-11eb-aebf-c306684b328d,panelIndex:%275%27,type:search,version:%277.13.1%27),(embeddableConfig:(enhancements:()),gridData:(h:15,i:%276%27,w:24,x:24,y:30),id:f852d570-619d-11eb-aebf-c306684b328d,panelIndex:%276%27,type:search,version:%277.13.1%27)),query:(language:kuery,query:%27%27),tags:!(),timeRestore:!f,title:shakespeare_dashboard,viewMode:view)"},"coreMigrationVersion":"7.13.5","id":"32a03249ec3a048108d4b5a427a37fc8","references":[],"sort":[1651599812857,128],"type":"url","updated_at":"2022-05-03T17:43:32.857Z","version":"WzY4LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"shakespeare_piechart","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"shakespeare_piechart\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":false,\"labels\":{\"show\":true,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"play_name\",\"size\":15,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.5","id":"33736660-619e-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,130],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzY5LDFd"} +{"attributes":{"color":"#7b01cf","description":"","name":"By reference"},"coreMigrationVersion":"7.13.5","id":"39d2c190-ca2b-11eb-bf5e-3de94e83d4f0","references":[],"sort":[1651599812857,131],"type":"tag","updated_at":"2022-05-03T17:43:32.857Z","version":"WzcwLDFd"} +{"attributes":{"columns":[],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[{\"meta\":{\"alias\":null,\"negate\":false,\"disabled\":false,\"type\":\"phrase\",\"key\":\"ip\",\"params\":{\"query\":\"57.237.11.219\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match_phrase\":{\"ip\":\"57.237.11.219\"}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["@timestamp","desc"]],"title":"drilldown_saved_search","version":1},"coreMigrationVersion":"7.13.5","id":"4acce030-ca2a-11eb-bf5e-3de94e83d4f0","migrationVersion":{"search":"7.9.3"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"}],"sort":[1651612879859,1007],"type":"search","updated_at":"2022-05-03T21:21:19.859Z","version":"WzU2NywxXQ=="} +{"attributes":{"description":"","layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"e0d51731-2bb3-4fed-92af-65f93c3e7e58\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"},{\"sourceDescriptor\":{\"geoField\":\"geo.coordinates\",\"filterByMapBounds\":true,\"scalingType\":\"CLUSTERS\",\"topHitsSplitField\":\"\",\"topHitsSize\":1,\"id\":\"142e0a6b-53c9-4f66-a65d-fced755318de\",\"type\":\"ES_SEARCH\",\"applyGlobalQuery\":true,\"applyGlobalTime\":true,\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"id\":\"ca96ce4a-4e73-46a5-bcc8-99a39d227030\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#CA8EAE\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#934193\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true},\"type\":\"BLENDED_VECTOR\",\"joins\":[]}]","mapStateJSON":"{\"zoom\":1.38,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"2014-07-15T12:33:21.084Z\",\"to\":\"2019-01-28T03:18:12.440Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"backgroundColor\":\"#ffffff\",\"disableInteractive\":false,\"disableTooltipControl\":false,\"hideToolbarOverlay\":false,\"hideLayerControl\":false,\"hideViewControl\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showScaleControl\":false,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}","title":"Logstash_map_by_reference","uiStateJSON":"{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}"},"coreMigrationVersion":"7.13.5","id":"a53a2db0-ca2b-11eb-bf5e-3de94e83d4f0","migrationVersion":{"map":"7.12.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"layer_1_source_index_pattern","type":"index-pattern"}],"sort":[1651612954243,1037],"type":"map","updated_at":"2022-05-03T21:22:34.243Z","version":"WzYwOSwxXQ=="} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.5\",\"type\":\"search\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"7c29a321-2a9a-412b-9ed1-1d0a1f66ea63\"},\"panelIndex\":\"7c29a321-2a9a-412b-9ed1-1d0a1f66ea63\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7c29a321-2a9a-412b-9ed1-1d0a1f66ea63\"},{\"version\":\"7.13.5\",\"type\":\"search\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"f2d1feb1-d807-46b1-90ac-96d4a9edb6b1\"},\"panelIndex\":\"f2d1feb1-d807-46b1-90ac-96d4a9edb6b1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_f2d1feb1-d807-46b1-90ac-96d4a9edb6b1\"},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"a3530107-8b1c-4e94-8f99-e239fa40a09c\"},\"panelIndex\":\"a3530107-8b1c-4e94-8f99-e239fa40a09c\",\"embeddableConfig\":{\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"b14188e0-53d6-433e-874f-b1be7c97487c\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"by_reference_going_to_value\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}},{\"eventId\":\"60cba413-1793-4dd3-b072-9d53655d5522\",\"triggers\":[\"SELECT_RANGE_TRIGGER\"],\"action\":{\"name\":\"Goto_Discover\",\"config\":{\"url\":{\"template\":\"http://localhost:5601/app/discover#/view/b3288100-ca2c-11eb-bf5e-3de94e83d4f0?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&_a=(columns:!(),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'43fcac20-ca27-11eb-bf5e-3de94e83d4f0',key:geo.dest,negate:!f,params:(query:US),type:phrase),query:(match_phrase:(geo.dest:US)))),index:'43fcac20-ca27-11eb-bf5e-3de94e83d4f0',interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))\"},\"openInNewTab\":true,\"encodeUrl\":true},\"factoryId\":\"URL_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_a3530107-8b1c-4e94-8f99-e239fa40a09c\"},{\"version\":\"7.13.5\",\"type\":\"map\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"77245314-9495-4625-9f53-0946150e26d4\"},\"panelIndex\":\"77245314-9495-4625-9f53-0946150e26d4\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":19.94277,\"lon\":0,\"zoom\":1.38},\"mapBuffer\":{\"minLon\":-238.27568,\"minLat\":-74.644155,\"maxLon\":238.27568,\"maxLat\":102.864625},\"isLayerTOCOpen\":true,\"openTOCDetails\":[],\"hiddenLayers\":[],\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"9b61b9d4-20a3-4bca-9697-1097c524a943\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"By_reference_to_value\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_77245314-9495-4625-9f53-0946150e26d4\"}]","timeRestore":false,"title":"by_reference_drilldown","version":1},"coreMigrationVersion":"7.13.5","id":"3b844220-ca2b-11eb-bf5e-3de94e83d4f0","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"4acce030-ca2a-11eb-bf5e-3de94e83d4f0","name":"7c29a321-2a9a-412b-9ed1-1d0a1f66ea63:panel_7c29a321-2a9a-412b-9ed1-1d0a1f66ea63","type":"search"},{"id":"0abce1c0-ca2a-11eb-bf5e-3de94e83d4f0","name":"f2d1feb1-d807-46b1-90ac-96d4a9edb6b1:panel_f2d1feb1-d807-46b1-90ac-96d4a9edb6b1","type":"search"},{"id":"1885abb0-ca2b-11eb-bf5e-3de94e83d4f0","name":"a3530107-8b1c-4e94-8f99-e239fa40a09c:panel_a3530107-8b1c-4e94-8f99-e239fa40a09c","type":"visualization"},{"id":"08dec860-ca29-11eb-bf5e-3de94e83d4f0","name":"a3530107-8b1c-4e94-8f99-e239fa40a09c:drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:b14188e0-53d6-433e-874f-b1be7c97487c:dashboardId","type":"dashboard"},{"id":"a53a2db0-ca2b-11eb-bf5e-3de94e83d4f0","name":"77245314-9495-4625-9f53-0946150e26d4:panel_77245314-9495-4625-9f53-0946150e26d4","type":"map"},{"id":"35ce3b30-ca29-11eb-bf5e-3de94e83d4f0","name":"77245314-9495-4625-9f53-0946150e26d4:drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:9b61b9d4-20a3-4bca-9697-1097c524a943:dashboardId","type":"dashboard"},{"id":"39d2c190-ca2b-11eb-bf5e-3de94e83d4f0","name":"tag-39d2c190-ca2b-11eb-bf5e-3de94e83d4f0","type":"tag"}],"sort":[1651612956152,1046],"type":"dashboard","updated_at":"2022-05-03T21:22:36.152Z","version":"WzYxNSwxXQ=="} +{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"a7a8f2fb-066e-4023-9755-821e84560b4a":{"columnOrder":["ee46f645-0af0-4b5d-8ed3-2557c98c9c12","91859a54-9b88-4478-8c80-0779fe165fba","62a4dea1-fab9-45ff-93e0-b99cfff719d5"],"columns":{"62a4dea1-fab9-45ff-93e0-b99cfff719d5":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"91859a54-9b88-4478-8c80-0779fe165fba":{"dataType":"string","isBucketed":true,"label":"Top values of play_name","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"62a4dea1-fab9-45ff-93e0-b99cfff719d5","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"play_name"},"ee46f645-0af0-4b5d-8ed3-2557c98c9c12":{"dataType":"string","isBucketed":true,"label":"Top values of speaker","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"62a4dea1-fab9-45ff-93e0-b99cfff719d5","type":"column"},"orderDirection":"desc","otherBucket":true,"size":25},"scale":"ordinal","sourceField":"speaker"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"layers":[{"categoryDisplay":"default","groups":["ee46f645-0af0-4b5d-8ed3-2557c98c9c12","ee46f645-0af0-4b5d-8ed3-2557c98c9c12","ee46f645-0af0-4b5d-8ed3-2557c98c9c12","ee46f645-0af0-4b5d-8ed3-2557c98c9c12","91859a54-9b88-4478-8c80-0779fe165fba"],"layerId":"a7a8f2fb-066e-4023-9755-821e84560b4a","legendDisplay":"default","metric":"62a4dea1-fab9-45ff-93e0-b99cfff719d5","nestedLegend":false,"numberDisplay":"percent"}],"palette":{"name":"kibana_palette","type":"palette"},"shape":"pie"}},"title":"lens_shakespeare_piechart","visualizationType":"lnsPie"},"coreMigrationVersion":"7.13.5","id":"b5bd5050-be31-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-a7a8f2fb-066e-4023-9755-821e84560b4a","type":"index-pattern"},{"id":"42b4cec0-be32-11eb-9520-1b4c3ca6a781","name":"tag-ref-42b4cec0-be32-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,135],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"Wzc1LDFd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"c4b1daae-a3af-4136-969e-8485d4ba53f9\"},\"panelIndex\":\"c4b1daae-a3af-4136-969e-8485d4ba53f9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_c4b1daae-a3af-4136-969e-8485d4ba53f9\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"f092b002-182e-49b8-bcc4-58f5233e041b\"},\"panelIndex\":\"f092b002-182e-49b8-bcc4-58f5233e041b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_f092b002-182e-49b8-bcc4-58f5233e041b\"}]","refreshInterval":{"pause":true,"value":0},"timeFrom":"2015-09-20T01:56:56.132Z","timeRestore":true,"timeTo":"2015-09-21T11:18:20.471Z","title":"lens_shakespeare_dashboard","version":1},"coreMigrationVersion":"7.13.5","id":"43fae350-be32-11eb-9520-1b4c3ca6a781","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"b5bd5050-be31-11eb-9520-1b4c3ca6a781","name":"c4b1daae-a3af-4136-969e-8485d4ba53f9:panel_c4b1daae-a3af-4136-969e-8485d4ba53f9","type":"lens"},{"id":"31e9f2f0-be32-11eb-9520-1b4c3ca6a781","name":"f092b002-182e-49b8-bcc4-58f5233e041b:panel_f092b002-182e-49b8-bcc4-58f5233e041b","type":"lens"},{"id":"42b4cec0-be32-11eb-9520-1b4c3ca6a781","name":"tag-42b4cec0-be32-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,139],"type":"dashboard","updated_at":"2022-05-03T17:43:32.857Z","version":"Wzc2LDFd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"3\",\"w\":24,\"x\":0,\"y\":15},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"4\",\"w\":24,\"x\":24,\"y\":15},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"5\",\"w\":24,\"x\":0,\"y\":30},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"6\",\"w\":24,\"x\":24,\"y\":30},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"7\",\"w\":24,\"x\":0,\"y\":45},\"panelIndex\":\"7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"8\",\"w\":24,\"x\":24,\"y\":45},\"panelIndex\":\"8\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"9\",\"w\":24,\"x\":0,\"y\":60},\"panelIndex\":\"9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"10\",\"w\":24,\"x\":24,\"y\":60},\"panelIndex\":\"10\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_10\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"11\",\"w\":24,\"x\":0,\"y\":75},\"panelIndex\":\"11\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_11\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"12\",\"w\":24,\"x\":24,\"y\":75},\"panelIndex\":\"12\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_12\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"13\",\"w\":24,\"x\":0,\"y\":90},\"panelIndex\":\"13\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"14\",\"w\":24,\"x\":24,\"y\":90},\"panelIndex\":\"14\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_14\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"15\",\"w\":24,\"x\":0,\"y\":105},\"panelIndex\":\"15\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_15\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"16\",\"w\":24,\"x\":24,\"y\":105},\"panelIndex\":\"16\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_16\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"17\",\"w\":24,\"x\":0,\"y\":120},\"panelIndex\":\"17\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_17\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"18\",\"w\":24,\"x\":24,\"y\":120},\"panelIndex\":\"18\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_18\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"19\",\"w\":24,\"x\":0,\"y\":135},\"panelIndex\":\"19\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_19\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"h\":15,\"i\":\"20\",\"w\":24,\"x\":24,\"y\":135},\"panelIndex\":\"20\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_20\"}]","timeRestore":false,"title":"logstash_dashboard_withouttime","version":1},"coreMigrationVersion":"7.13.5","id":"5d3410c0-6249-11eb-aebf-c306684b328d","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"36b91810-6239-11eb-aebf-c306684b328d","name":"1:panel_1","type":"visualization"},{"id":"0a274320-61cc-11eb-aebf-c306684b328d","name":"2:panel_2","type":"visualization"},{"id":"e4aef350-623d-11eb-aebf-c306684b328d","name":"3:panel_3","type":"visualization"},{"id":"f92e5630-623e-11eb-aebf-c306684b328d","name":"4:panel_4","type":"visualization"},{"id":"9853d4d0-623d-11eb-aebf-c306684b328d","name":"5:panel_5","type":"visualization"},{"id":"6ecb33b0-623d-11eb-aebf-c306684b328d","name":"6:panel_6","type":"visualization"},{"id":"b8e35c80-623c-11eb-aebf-c306684b328d","name":"7:panel_7","type":"visualization"},{"id":"f1bc75d0-6239-11eb-aebf-c306684b328d","name":"8:panel_8","type":"visualization"},{"id":"0d8a8860-623a-11eb-aebf-c306684b328d","name":"9:panel_9","type":"visualization"},{"id":"d79fe3d0-6239-11eb-aebf-c306684b328d","name":"10:panel_10","type":"visualization"},{"id":"318375a0-6240-11eb-aebf-c306684b328d","name":"11:panel_11","type":"visualization"},{"id":"e461eb20-6245-11eb-aebf-c306684b328d","name":"12:panel_12","type":"visualization"},{"id":"25bdc750-6242-11eb-aebf-c306684b328d","name":"13:panel_13","type":"visualization"},{"id":"71dd7bc0-6248-11eb-aebf-c306684b328d","name":"14:panel_14","type":"visualization"},{"id":"6aea48a0-6240-11eb-aebf-c306684b328d","name":"15:panel_15","type":"visualization"},{"id":"32b681f0-6241-11eb-aebf-c306684b328d","name":"16:panel_16","type":"visualization"},{"id":"ccca99e0-6244-11eb-aebf-c306684b328d","name":"17:panel_17","type":"visualization"},{"id":"a4d7be80-6245-11eb-aebf-c306684b328d","name":"18:panel_18","type":"visualization"},{"id":"c94d8440-6248-11eb-aebf-c306684b328d","name":"19:panel_19","type":"visualization"},{"id":"db6226f0-61c0-11eb-aebf-c306684b328d","name":"20:panel_20","type":"search"}],"sort":[1651599812857,160],"type":"dashboard","updated_at":"2022-05-03T17:43:32.857Z","version":"Wzc3LDFd"} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"shakespeare_tag_cloud","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"shakespeare_tag_cloud\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"multiple\",\"minFontSize\":59,\"maxFontSize\":100,\"showLabel\":true},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"type.keyword\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}"},"coreMigrationVersion":"7.13.5","id":"622ac7f0-619e-11eb-aebf-c306684b328d","migrationVersion":{"visualization":"7.13.1"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,162],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"Wzc4LDFd"} +{"attributes":{"numLinks":4,"numVertices":5,"title":"logstash_graph","version":1,"wsState":"\"{\\\"selectedFields\\\":[{\\\"name\\\":\\\"machine.os.raw\\\",\\\"hopSize\\\":5,\\\"lastValidHopSize\\\":5,\\\"color\\\":\\\"#B9A888\\\",\\\"selected\\\":true,\\\"iconClass\\\":\\\"fa-folder-open-o\\\"},{\\\"name\\\":\\\"response.raw\\\",\\\"hopSize\\\":5,\\\"lastValidHopSize\\\":5,\\\"color\\\":\\\"#D6BF57\\\",\\\"selected\\\":true,\\\"iconClass\\\":\\\"fa-folder-open-o\\\"}],\\\"blocklist\\\":[],\\\"vertices\\\":[{\\\"x\\\":461.96184642905024,\\\"y\\\":284.02313214227325,\\\"label\\\":\\\"osx\\\",\\\"color\\\":\\\"#B9A888\\\",\\\"field\\\":\\\"machine.os.raw\\\",\\\"term\\\":\\\"osx\\\",\\\"parent\\\":null,\\\"size\\\":15},{\\\"x\\\":383.946159835112,\\\"y\\\":375.6063135315976,\\\"label\\\":\\\"503\\\",\\\"color\\\":\\\"#D6BF57\\\",\\\"field\\\":\\\"response.raw\\\",\\\"term\\\":\\\"503\\\",\\\"parent\\\":null,\\\"size\\\":15},{\\\"x\\\":287.104700756828,\\\"y\\\":324.1245253249895,\\\"label\\\":\\\"win 7\\\",\\\"color\\\":\\\"#B9A888\\\",\\\"field\\\":\\\"machine.os.raw\\\",\\\"term\\\":\\\"win 7\\\",\\\"parent\\\":null,\\\"size\\\":15},{\\\"x\\\":487.9986107998273,\\\"y\\\":407.07546535764254,\\\"label\\\":\\\"ios\\\",\\\"color\\\":\\\"#B9A888\\\",\\\"field\\\":\\\"machine.os.raw\\\",\\\"term\\\":\\\"ios\\\",\\\"parent\\\":null,\\\"size\\\":15},{\\\"x\\\":302.35059551806023,\\\"y\\\":211.66825720913607,\\\"label\\\":\\\"200\\\",\\\"color\\\":\\\"#D6BF57\\\",\\\"field\\\":\\\"response.raw\\\",\\\"term\\\":\\\"200\\\",\\\"parent\\\":null,\\\"size\\\":15}],\\\"links\\\":[{\\\"weight\\\":0.000881324009872165,\\\"width\\\":7.983523640193488,\\\"source\\\":4,\\\"target\\\":2},{\\\"weight\\\":0.000023386835221992895,\\\"width\\\":2,\\\"source\\\":1,\\\"target\\\":0},{\\\"weight\\\":0.0011039286029480653,\\\"width\\\":2,\\\"source\\\":1,\\\"target\\\":2},{\\\"weight\\\":0.000045596928960694605,\\\"width\\\":2,\\\"source\\\":1,\\\"target\\\":3}],\\\"urlTemplates\\\":[{\\\"url\\\":\\\"/app/discover#/?_a=(columns%3A!(_source)%2Cindex%3A%2756b34100-619d-11eb-aebf-c306684b328d%27%2Cinterval%3Aauto%2Cquery%3A(language%3Akuery%2Cquery%3A{{gquery}})%2Csort%3A!(_score%2Cdesc))\\\",\\\"description\\\":\\\"Machine OS win 7\\\",\\\"isDefault\\\":false,\\\"encoderID\\\":\\\"kql\\\",\\\"iconClass\\\":\\\"fa-share-alt\\\"}],\\\"exploreControls\\\":{\\\"useSignificance\\\":true,\\\"sampleSize\\\":2000,\\\"timeoutMillis\\\":5000,\\\"maxValuesPerDoc\\\":1,\\\"minDocCount\\\":3},\\\"indexPatternRefName\\\":\\\"indexPattern_0\\\"}\""},"coreMigrationVersion":"7.13.5","id":"6afc4b40-be5c-11eb-9520-1b4c3ca6a781","migrationVersion":{"graph-workspace":"7.11.0"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexPattern_0","type":"index-pattern"}],"sort":[1651599812857,164],"type":"graph-workspace","updated_at":"2022-05-03T17:43:32.857Z","version":"Wzc5LDFd"} +{"attributes":{"buildNum":39457,"defaultIndex":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0"},"coreMigrationVersion":"7.13.5","id":"7.12.1","migrationVersion":{"config":"7.13.0"},"references":[],"sort":[1651599812857,165],"type":"config","updated_at":"2022-05-03T17:43:32.857Z","version":"WzgwLDFd"} +{"attributes":{"accessibility:disableAnimations":true,"buildNum":null,"dateFormat:tz":"UTC","defaultIndex":"56b34100-619d-11eb-aebf-c306684b328d","visualization:visualize:legacyChartsLibrary":true},"coreMigrationVersion":"7.13.5","id":"7.13.1","migrationVersion":{"config":"7.13.0"},"references":[],"sort":[1651599812857,166],"type":"config","updated_at":"2022-05-03T17:43:32.857Z","version":"WzgxLDFd"} +{"attributes":{"buildNum":40943,"defaultIndex":"43fcac20-ca27-11eb-bf5e-3de94e83d4f0"},"coreMigrationVersion":"7.13.5","id":"7.13.2","migrationVersion":{"config":"7.13.0"},"references":[],"sort":[1651599812857,167],"type":"config","updated_at":"2022-05-03T17:43:32.857Z","version":"WzgyLDFd"} +{"attributes":{"buildNum":9007199254740991,"defaultIndex":"56b34100-619d-11eb-aebf-c306684b328d"},"coreMigrationVersion":"7.13.5","id":"7.13.5","migrationVersion":{"config":"7.13.0"},"references":[],"sort":[1651613092912,1086],"type":"config","updated_at":"2022-05-03T21:24:52.912Z","version":"WzYzOCwxXQ=="} +{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":false,\"type\":\"phrase\",\"key\":\"text_entry\",\"value\":\"Christendom.\",\"params\":{\"query\":\"Christendom.\",\"type\":\"phrase\"},\"disabled\":false,\"alias\":null,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"text_entry\":{\"query\":\"Christendom.\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["_score","desc"]],"title":"shakespeare_saved_search","version":1},"coreMigrationVersion":"7.13.5","id":"712ebbe0-619d-11eb-aebf-c306684b328d","migrationVersion":{"search":"7.9.3"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"}],"sort":[1651599812857,170],"type":"search","updated_at":"2022-05-03T17:43:32.857Z","version":"WzgzLDFd"} +{"attributes":{"columns":["play_name","speaker"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"speaker:\\\"GLOUCESTER\\\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["_score","desc"]],"title":"shakespeare_saved_lucene_search","version":1},"coreMigrationVersion":"7.13.5","id":"ddacc820-619d-11eb-aebf-c306684b328d","migrationVersion":{"search":"7.9.3"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,172],"type":"search","updated_at":"2022-05-03T17:43:32.857Z","version":"Wzg0LDFd"} +{"attributes":{"columns":["_source"],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"text_entry :\\\"MORDAKE THE EARL OF FIFE, AND ELDEST SON\\\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["_score","desc"]],"title":"shakespeare_saved_kql_search","version":1},"coreMigrationVersion":"7.13.5","id":"f852d570-619d-11eb-aebf-c306684b328d","migrationVersion":{"search":"7.9.3"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651599812857,174],"type":"search","updated_at":"2022-05-03T17:43:32.857Z","version":"Wzg1LDFd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"3\",\"w\":24,\"x\":0,\"y\":15},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"h\":15,\"i\":\"4\",\"w\":24,\"x\":24,\"y\":15},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"h\":15,\"i\":\"5\",\"w\":24,\"x\":0,\"y\":30},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"h\":15,\"i\":\"6\",\"w\":24,\"x\":24,\"y\":30},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"}]","timeRestore":false,"title":"shakespeare_dashboard","version":1},"coreMigrationVersion":"7.13.5","id":"73398a90-619e-11eb-aebf-c306684b328d","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"185283c0-619e-11eb-aebf-c306684b328d","name":"1:panel_1","type":"visualization"},{"id":"33736660-619e-11eb-aebf-c306684b328d","name":"2:panel_2","type":"visualization"},{"id":"622ac7f0-619e-11eb-aebf-c306684b328d","name":"3:panel_3","type":"visualization"},{"id":"712ebbe0-619d-11eb-aebf-c306684b328d","name":"4:panel_4","type":"search"},{"id":"ddacc820-619d-11eb-aebf-c306684b328d","name":"5:panel_5","type":"search"},{"id":"f852d570-619d-11eb-aebf-c306684b328d","name":"6:panel_6","type":"search"}],"sort":[1651599812857,181],"type":"dashboard","updated_at":"2022-05-03T17:43:32.857Z","version":"Wzg2LDFd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"lucene\",\"query\":\"\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"geo.srcdest\",\"value\":\"IN:US\",\"params\":{\"query\":\"IN:US\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"geo.srcdest\":{\"query\":\"IN:US\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}]}"},"optionsJSON":"{\"darkTheme\":false,\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"1\",\"w\":24,\"x\":0,\"y\":0},\"panelIndex\":\"1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"2\",\"w\":24,\"x\":24,\"y\":0},\"panelIndex\":\"2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"3\",\"w\":24,\"x\":0,\"y\":15},\"panelIndex\":\"3\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"4\",\"w\":24,\"x\":24,\"y\":15},\"panelIndex\":\"4\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"5\",\"w\":24,\"x\":0,\"y\":30},\"panelIndex\":\"5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"6\",\"w\":24,\"x\":24,\"y\":30},\"panelIndex\":\"6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"7\",\"w\":24,\"x\":0,\"y\":45},\"panelIndex\":\"7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_7\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"8\",\"w\":24,\"x\":24,\"y\":45},\"panelIndex\":\"8\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"9\",\"w\":24,\"x\":0,\"y\":60},\"panelIndex\":\"9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"10\",\"w\":24,\"x\":24,\"y\":60},\"panelIndex\":\"10\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_10\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"11\",\"w\":24,\"x\":0,\"y\":75},\"panelIndex\":\"11\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_11\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"12\",\"w\":24,\"x\":24,\"y\":75},\"panelIndex\":\"12\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_12\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"13\",\"w\":24,\"x\":0,\"y\":90},\"panelIndex\":\"13\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"14\",\"w\":24,\"x\":24,\"y\":90},\"panelIndex\":\"14\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_14\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"15\",\"w\":24,\"x\":0,\"y\":105},\"panelIndex\":\"15\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_15\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"16\",\"w\":24,\"x\":24,\"y\":105},\"panelIndex\":\"16\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_16\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"17\",\"w\":24,\"x\":0,\"y\":120},\"panelIndex\":\"17\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_17\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"18\",\"w\":24,\"x\":24,\"y\":120},\"panelIndex\":\"18\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_18\"},{\"version\":\"7.3.0\",\"type\":\"visualization\",\"gridData\":{\"h\":15,\"i\":\"19\",\"w\":24,\"x\":0,\"y\":135},\"panelIndex\":\"19\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_19\"},{\"version\":\"7.3.0\",\"type\":\"search\",\"gridData\":{\"h\":15,\"i\":\"20\",\"w\":24,\"x\":24,\"y\":135},\"panelIndex\":\"20\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_20\"}]","timeRestore":false,"title":"logstash_dashboardwithfilters","version":1},"coreMigrationVersion":"7.13.5","id":"79794f20-6249-11eb-aebf-c306684b328d","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"},{"id":"36b91810-6239-11eb-aebf-c306684b328d","name":"1:panel_1","type":"visualization"},{"id":"0a274320-61cc-11eb-aebf-c306684b328d","name":"2:panel_2","type":"visualization"},{"id":"e4aef350-623d-11eb-aebf-c306684b328d","name":"3:panel_3","type":"visualization"},{"id":"f92e5630-623e-11eb-aebf-c306684b328d","name":"4:panel_4","type":"visualization"},{"id":"9853d4d0-623d-11eb-aebf-c306684b328d","name":"5:panel_5","type":"visualization"},{"id":"6ecb33b0-623d-11eb-aebf-c306684b328d","name":"6:panel_6","type":"visualization"},{"id":"b8e35c80-623c-11eb-aebf-c306684b328d","name":"7:panel_7","type":"visualization"},{"id":"f1bc75d0-6239-11eb-aebf-c306684b328d","name":"8:panel_8","type":"visualization"},{"id":"0d8a8860-623a-11eb-aebf-c306684b328d","name":"9:panel_9","type":"visualization"},{"id":"d79fe3d0-6239-11eb-aebf-c306684b328d","name":"10:panel_10","type":"visualization"},{"id":"318375a0-6240-11eb-aebf-c306684b328d","name":"11:panel_11","type":"visualization"},{"id":"e461eb20-6245-11eb-aebf-c306684b328d","name":"12:panel_12","type":"visualization"},{"id":"25bdc750-6242-11eb-aebf-c306684b328d","name":"13:panel_13","type":"visualization"},{"id":"71dd7bc0-6248-11eb-aebf-c306684b328d","name":"14:panel_14","type":"visualization"},{"id":"6aea48a0-6240-11eb-aebf-c306684b328d","name":"15:panel_15","type":"visualization"},{"id":"32b681f0-6241-11eb-aebf-c306684b328d","name":"16:panel_16","type":"visualization"},{"id":"ccca99e0-6244-11eb-aebf-c306684b328d","name":"17:panel_17","type":"visualization"},{"id":"a4d7be80-6245-11eb-aebf-c306684b328d","name":"18:panel_18","type":"visualization"},{"id":"c94d8440-6248-11eb-aebf-c306684b328d","name":"19:panel_19","type":"visualization"},{"id":"db6226f0-61c0-11eb-aebf-c306684b328d","name":"20:panel_20","type":"search"}],"sort":[1651599812857,203],"type":"dashboard","updated_at":"2022-05-03T17:43:32.857Z","version":"Wzg3LDFd"} +{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"037b7937-790b-4d2d-94a5-7f5837a6ef05":{"columnOrder":["b3d46616-75e0-419e-97ea-91148961ef94","025a0fb3-dc44-4f5c-b517-2d71d3f26f14","c476db14-0cc1-40ec-863e-d2779256a407"],"columns":{"025a0fb3-dc44-4f5c-b517-2d71d3f26f14":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"@timestamp"},"b3d46616-75e0-419e-97ea-91148961ef94":{"dataType":"string","isBucketed":true,"label":"Top values of geo.srcdest","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"c476db14-0cc1-40ec-863e-d2779256a407","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"geo.srcdest"},"c476db14-0cc1-40ec-863e-d2779256a407":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"lucene","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["c476db14-0cc1-40ec-863e-d2779256a407"],"layerId":"037b7937-790b-4d2d-94a5-7f5837a6ef05","position":"top","seriesType":"bar_stacked","showGridlines":false,"splitAccessor":"b3d46616-75e0-419e-97ea-91148961ef94","xAccessor":"025a0fb3-dc44-4f5c-b517-2d71d3f26f14"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"lens_verticalstacked","visualizationType":"lnsXY"},"coreMigrationVersion":"7.13.5","id":"8dc19b50-be32-11eb-9520-1b4c3ca6a781","migrationVersion":{"lens":"7.13.1"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"indexpattern-datasource-layer-037b7937-790b-4d2d-94a5-7f5837a6ef05","type":"index-pattern"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-ref-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,207],"type":"lens","updated_at":"2022-05-03T17:43:32.857Z","version":"Wzg4LDFd"} +{"attributes":{"columns":[],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[{\"meta\":{\"alias\":null,\"negate\":false,\"disabled\":false,\"type\":\"phrase\",\"key\":\"geo.dest\",\"params\":{\"query\":\"US\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match_phrase\":{\"geo.dest\":\"US\"}},\"$state\":{\"store\":\"appState\"}}],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[["@timestamp","desc"]],"title":"drilldown_logstash","version":1},"coreMigrationVersion":"7.13.5","id":"b3288100-ca2c-11eb-bf5e-3de94e83d4f0","migrationVersion":{"search":"7.9.3"},"references":[{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index","type":"index-pattern"}],"sort":[1651612907652,1002],"type":"search","updated_at":"2022-05-03T21:21:47.652Z","version":"WzU3NiwxXQ=="} +{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{}"},"title":"logstash_timelion_panel","uiStateJSON":"{}","version":1,"visState":"{\"title\":\"logstash_timelion_panel\",\"type\":\"timelion\",\"aggs\":[],\"params\":{\"expression\":\".es(index=logstash-*, \\\"sum:bytes\\\")\",\"interval\":\"auto\"}}"},"coreMigrationVersion":"7.13.5","id":"b3a44cd0-be5c-11eb-9520-1b4c3ca6a781","migrationVersion":{"visualization":"7.13.1"},"references":[],"sort":[1651599812857,208],"type":"visualization","updated_at":"2022-05-03T17:43:32.857Z","version":"WzkwLDFd"} +{"attributes":{"color":"#9170B8","description":"","name":"alltogether"},"coreMigrationVersion":"7.13.5","id":"be808cb0-be32-11eb-9520-1b4c3ca6a781","references":[],"sort":[1651599812857,209],"type":"tag","updated_at":"2022-05-03T17:43:32.857Z","version":"WzkxLDFd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"4d9e9a01-cdb8-4aef-afcb-50db52247bb1\"},\"panelIndex\":\"4d9e9a01-cdb8-4aef-afcb-50db52247bb1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4d9e9a01-cdb8-4aef-afcb-50db52247bb1\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"d9cab9c8-667e-4d34-821b-cbb070891956\"},\"panelIndex\":\"d9cab9c8-667e-4d34-821b-cbb070891956\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_d9cab9c8-667e-4d34-821b-cbb070891956\"}]","refreshInterval":{"pause":true,"value":0},"timeFrom":"2015-09-20T01:56:56.132Z","timeRestore":true,"timeTo":"2015-09-21T11:18:20.471Z","title":"lens_combined_dashboard","version":1},"coreMigrationVersion":"7.13.5","id":"bfb3dc90-be32-11eb-9520-1b4c3ca6a781","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"8dc19b50-be32-11eb-9520-1b4c3ca6a781","name":"4d9e9a01-cdb8-4aef-afcb-50db52247bb1:panel_4d9e9a01-cdb8-4aef-afcb-50db52247bb1","type":"lens"},{"id":"b5bd5050-be31-11eb-9520-1b4c3ca6a781","name":"d9cab9c8-667e-4d34-821b-cbb070891956:panel_d9cab9c8-667e-4d34-821b-cbb070891956","type":"lens"},{"id":"be808cb0-be32-11eb-9520-1b4c3ca6a781","name":"tag-be808cb0-be32-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,213],"type":"dashboard","updated_at":"2022-05-03T17:43:32.857Z","version":"WzkyLDFd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"2e80716f-c1b6-46f2-be2b-35db744b5031\"},\"panelIndex\":\"2e80716f-c1b6-46f2-be2b-35db744b5031\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsPie\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"26e2cf99-d931-4320-9e15-9dbc148f3534\":{\"columns\":{\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\":{\"label\":\"Top values of url.raw\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"url.raw\",\"isBucketed\":true,\"params\":{\"size\":20,\"orderBy\":{\"type\":\"column\",\"columnId\":\"beb72af1-239c-46d8-823b-b00d1e2ace43\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"beb72af1-239c-46d8-823b-b00d1e2ace43\":{\"label\":\"Unique count of geo.srcdest\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"geo.srcdest\",\"isBucketed\":false}},\"columnOrder\":[\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"beb72af1-239c-46d8-823b-b00d1e2ace43\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"shape\":\"donut\",\"layers\":[{\"layerId\":\"26e2cf99-d931-4320-9e15-9dbc148f3534\",\"groups\":[\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\"],\"metric\":\"beb72af1-239c-46d8-823b-b00d1e2ace43\",\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"default\",\"nestedLegend\":false}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-26e2cf99-d931-4320-9e15-9dbc148f3534\"}]},\"enhancements\":{},\"type\":\"lens\"},\"panelRefName\":\"panel_2e80716f-c1b6-46f2-be2b-35db744b5031\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"da8843e0-6789-4aae-bcd0-81f270538719\"},\"panelIndex\":\"da8843e0-6789-4aae-bcd0-81f270538719\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_da8843e0-6789-4aae-bcd0-81f270538719\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"adcd4418-7299-4efa-b369-5f71a7b4ebe0\"},\"panelIndex\":\"adcd4418-7299-4efa-b369-5f71a7b4ebe0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_adcd4418-7299-4efa-b369-5f71a7b4ebe0\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"869754a7-edf0-478f-a7f1-80374f63108a\"},\"panelIndex\":\"869754a7-edf0-478f-a7f1-80374f63108a\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_869754a7-edf0-478f-a7f1-80374f63108a\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":30,\"w\":24,\"h\":15,\"i\":\"67111cf4-338e-453f-8621-e8dea64082d1\"},\"panelIndex\":\"67111cf4-338e-453f-8621-e8dea64082d1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_67111cf4-338e-453f-8621-e8dea64082d1\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\"},\"panelIndex\":\"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":45,\"w\":24,\"h\":15,\"i\":\"88847944-ae1b-45fd-b102-3b45f9bea04b\"},\"panelIndex\":\"88847944-ae1b-45fd-b102-3b45f9bea04b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_88847944-ae1b-45fd-b102-3b45f9bea04b\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":45,\"w\":24,\"h\":15,\"i\":\"5a7924c7-eac0-4573-9199-fecec5b82e9e\"},\"panelIndex\":\"5a7924c7-eac0-4573-9199-fecec5b82e9e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5a7924c7-eac0-4573-9199-fecec5b82e9e\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":60,\"w\":24,\"h\":15,\"i\":\"f8f49591-f071-4a96-b1ed-cd65daff5648\"},\"panelIndex\":\"f8f49591-f071-4a96-b1ed-cd65daff5648\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_f8f49591-f071-4a96-b1ed-cd65daff5648\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":60,\"w\":24,\"h\":15,\"i\":\"9f357f47-c2a0-421f-a456-9583c40837ab\"},\"panelIndex\":\"9f357f47-c2a0-421f-a456-9583c40837ab\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9f357f47-c2a0-421f-a456-9583c40837ab\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":75,\"w\":24,\"h\":15,\"i\":\"6cb383e9-1e80-44f9-80d5-7b8c585668db\"},\"panelIndex\":\"6cb383e9-1e80-44f9-80d5-7b8c585668db\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6cb383e9-1e80-44f9-80d5-7b8c585668db\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":75,\"w\":24,\"h\":15,\"i\":\"57f5f0bf-6610-4599-aad4-37484640b5e2\"},\"panelIndex\":\"57f5f0bf-6610-4599-aad4-37484640b5e2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_57f5f0bf-6610-4599-aad4-37484640b5e2\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":90,\"w\":24,\"h\":15,\"i\":\"32d3ab66-52e1-44e3-8c1f-1dccff3c5692\"},\"panelIndex\":\"32d3ab66-52e1-44e3-8c1f-1dccff3c5692\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_32d3ab66-52e1-44e3-8c1f-1dccff3c5692\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":90,\"w\":24,\"h\":15,\"i\":\"dd1718fd-74ee-4032-851b-db97e893825d\"},\"panelIndex\":\"dd1718fd-74ee-4032-851b-db97e893825d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_dd1718fd-74ee-4032-851b-db97e893825d\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":105,\"w\":24,\"h\":15,\"i\":\"98a556ee-078b-4e03-93a8-29996133cdcb\"},\"panelIndex\":\"98a556ee-078b-4e03-93a8-29996133cdcb\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"999a2d60-cb2a-451c-8d71-80d7e92e70fd\":{\"columns\":{\"ce9117a2-773c-474c-8fb1-18940cf58b38\":{\"label\":\"Top values of type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"type\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\"},\"orderDirection\":\"asc\",\"otherBucket\":true,\"missingBucket\":false}},\"a3d10552-e352-40d0-a156-e86112c0501a\":{\"label\":\"Top values of _type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"_type\",\"isBucketed\":true,\"params\":{\"size\":3,\"orderBy\":{\"type\":\"column\",\"columnId\":\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"},\"9c5db2f3-9eb0-4667-9a74-3318301de251\":{\"label\":\"Sum of bytes\",\"dataType\":\"number\",\"operationType\":\"sum\",\"sourceField\":\"bytes\",\"isBucketed\":false,\"scale\":\"ratio\"}},\"columnOrder\":[\"ce9117a2-773c-474c-8fb1-18940cf58b38\",\"a3d10552-e352-40d0-a156-e86112c0501a\",\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\",\"9c5db2f3-9eb0-4667-9a74-3318301de251\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"999a2d60-cb2a-451c-8d71-80d7e92e70fd\",\"accessors\":[\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\",\"9c5db2f3-9eb0-4667-9a74-3318301de251\"],\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"xAccessor\":\"ce9117a2-773c-474c-8fb1-18940cf58b38\",\"splitAccessor\":\"a3d10552-e352-40d0-a156-e86112c0501a\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-999a2d60-cb2a-451c-8d71-80d7e92e70fd\"}]},\"enhancements\":{},\"type\":\"lens\"}},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":105,\"w\":24,\"h\":15,\"i\":\"62a0f0b0-3589-4cef-807b-b1b4258b7a9b\"},\"panelIndex\":\"62a0f0b0-3589-4cef-807b-b1b4258b7a9b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_62a0f0b0-3589-4cef-807b-b1b4258b7a9b\"},{\"version\":\"7.13.1\",\"type\":\"map\",\"gridData\":{\"x\":0,\"y\":120,\"w\":24,\"h\":15,\"i\":\"dcc0defa-3376-465c-9b5b-2ba69528848c\"},\"panelIndex\":\"dcc0defa-3376-465c-9b5b-2ba69528848c\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":19.94277,\"lon\":0,\"zoom\":1.56},\"mapBuffer\":{\"minLon\":-210.32666,\"minLat\":-64.8435,\"maxLon\":210.32666,\"maxLat\":95.13806},\"isLayerTOCOpen\":true,\"openTOCDetails\":[],\"hiddenLayers\":[],\"enhancements\":{}},\"panelRefName\":\"panel_dcc0defa-3376-465c-9b5b-2ba69528848c\"},{\"version\":\"7.13.1\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":120,\"w\":24,\"h\":15,\"i\":\"dd21a674-ae3a-40f6-9d68-4e01361ea5e2\"},\"panelIndex\":\"dd21a674-ae3a-40f6-9d68-4e01361ea5e2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_dd21a674-ae3a-40f6-9d68-4e01361ea5e2\"}]","refreshInterval":{"pause":true,"value":0},"timeFrom":"2015-09-20T01:56:56.132Z","timeRestore":true,"timeTo":"2015-09-21T11:18:20.471Z","title":"timelion_lens_maps_dashboard_logstash","version":1},"coreMigrationVersion":"7.13.5","id":"c4ab2030-be5c-11eb-9520-1b4c3ca6a781","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"21905950-bd9f-11eb-9520-1b4c3ca6a781","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:panel_2e80716f-c1b6-46f2-be2b-35db744b5031","type":"lens"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:indexpattern-datasource-layer-26e2cf99-d931-4320-9e15-9dbc148f3534","type":"index-pattern"},{"id":"aa4b8da0-bd9f-11eb-9520-1b4c3ca6a781","name":"da8843e0-6789-4aae-bcd0-81f270538719:panel_da8843e0-6789-4aae-bcd0-81f270538719","type":"lens"},{"id":"2d3f1250-bd9f-11eb-9520-1b4c3ca6a781","name":"adcd4418-7299-4efa-b369-5f71a7b4ebe0:panel_adcd4418-7299-4efa-b369-5f71a7b4ebe0","type":"lens"},{"id":"edd5a560-bda4-11eb-9520-1b4c3ca6a781","name":"869754a7-edf0-478f-a7f1-80374f63108a:panel_869754a7-edf0-478f-a7f1-80374f63108a","type":"lens"},{"id":"2c25a450-bda5-11eb-9520-1b4c3ca6a781","name":"67111cf4-338e-453f-8621-e8dea64082d1:panel_67111cf4-338e-453f-8621-e8dea64082d1","type":"lens"},{"id":"e79116e0-bd9e-11eb-9520-1b4c3ca6a781","name":"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d:panel_13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d","type":"lens"},{"id":"974fb950-bda5-11eb-9520-1b4c3ca6a781","name":"88847944-ae1b-45fd-b102-3b45f9bea04b:panel_88847944-ae1b-45fd-b102-3b45f9bea04b","type":"lens"},{"id":"21905950-bd9f-11eb-9520-1b4c3ca6a781","name":"5a7924c7-eac0-4573-9199-fecec5b82e9e:panel_5a7924c7-eac0-4573-9199-fecec5b82e9e","type":"lens"},{"id":"51b63040-bda5-11eb-9520-1b4c3ca6a781","name":"f8f49591-f071-4a96-b1ed-cd65daff5648:panel_f8f49591-f071-4a96-b1ed-cd65daff5648","type":"lens"},{"id":"b00679c0-bda5-11eb-9520-1b4c3ca6a781","name":"9f357f47-c2a0-421f-a456-9583c40837ab:panel_9f357f47-c2a0-421f-a456-9583c40837ab","type":"lens"},{"id":"652ade10-bd9f-11eb-9520-1b4c3ca6a781","name":"6cb383e9-1e80-44f9-80d5-7b8c585668db:panel_6cb383e9-1e80-44f9-80d5-7b8c585668db","type":"lens"},{"id":"7f3b5fb0-be2f-11eb-9520-1b4c3ca6a781","name":"57f5f0bf-6610-4599-aad4-37484640b5e2:panel_57f5f0bf-6610-4599-aad4-37484640b5e2","type":"lens"},{"id":"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781","name":"32d3ab66-52e1-44e3-8c1f-1dccff3c5692:panel_32d3ab66-52e1-44e3-8c1f-1dccff3c5692","type":"lens"},{"id":"dd315430-be2f-11eb-9520-1b4c3ca6a781","name":"dd1718fd-74ee-4032-851b-db97e893825d:panel_dd1718fd-74ee-4032-851b-db97e893825d","type":"lens"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"98a556ee-078b-4e03-93a8-29996133cdcb:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"98a556ee-078b-4e03-93a8-29996133cdcb:indexpattern-datasource-layer-999a2d60-cb2a-451c-8d71-80d7e92e70fd","type":"index-pattern"},{"id":"0dbbf8b0-be3c-11eb-9520-1b4c3ca6a781","name":"62a0f0b0-3589-4cef-807b-b1b4258b7a9b:panel_62a0f0b0-3589-4cef-807b-b1b4258b7a9b","type":"lens"},{"id":"0c5974f0-be5c-11eb-9520-1b4c3ca6a781","name":"dcc0defa-3376-465c-9b5b-2ba69528848c:panel_dcc0defa-3376-465c-9b5b-2ba69528848c","type":"map"},{"id":"a4d7be80-6245-11eb-aebf-c306684b328d","name":"dd21a674-ae3a-40f6-9d68-4e01361ea5e2:panel_dd21a674-ae3a-40f6-9d68-4e01361ea5e2","type":"visualization"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,236],"type":"dashboard","updated_at":"2022-05-03T17:43:32.857Z","version":"WzkzLDFd"} +{"attributes":{"columns":[],"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"speaker :\\\"DUKE VINCENTIO\\\" \",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"sort":[],"title":"drilldown_shakes","version":1},"coreMigrationVersion":"7.13.5","id":"c4b9cc00-ca2a-11eb-bf5e-3de94e83d4f0","migrationVersion":{"search":"7.9.3"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"sort":[1651611630398,283],"type":"search","updated_at":"2022-05-03T21:00:30.398Z","version":"WzE1MCwxXQ=="} +{"attributes":{"@created":"2021-05-27T19:45:29.712Z","@timestamp":"2021-05-27T19:45:29.712Z","content":"{\"selectedNodes\":[{\"id\":\"element-56d2ba72-f227-4d04-9478-a1d6f0c7e601\",\"position\":{\"left\":20,\"top\":20,\"width\":500,\"height\":300,\"angle\":0,\"parent\":\"group-499b5982-25f4-4894-9540-1874a27d78e7\",\"type\":\"element\"},\"expression\":\"savedLens id=\\\"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781\\\" timerange={timerange from=\\\"now-15y\\\" to=\\\"now\\\"}\\n| render\",\"filter\":null,\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"savedLens\",\"arguments\":{\"id\":[\"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781\"],\"timerange\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"timerange\",\"arguments\":{\"from\":[\"now-15y\"],\"to\":[\"now\"]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-afbaa26e-10e7-47d4-bb41-b061dfdced2b\",\"position\":{\"left\":527,\"top\":20,\"width\":500,\"height\":300,\"angle\":0,\"parent\":\"group-499b5982-25f4-4894-9540-1874a27d78e7\",\"type\":\"element\"},\"expression\":\"savedVisualization id=\\\"0d8a8860-623a-11eb-aebf-c306684b328d\\\" timerange={timerange from=\\\"now-15y\\\" to=\\\"now\\\"}\\n| render\",\"filter\":null,\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"savedVisualization\",\"arguments\":{\"id\":[\"0d8a8860-623a-11eb-aebf-c306684b328d\"],\"timerange\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"timerange\",\"arguments\":{\"from\":[\"now-15y\"],\"to\":[\"now\"]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}","displayName":"element_canvas","help":"","image":"","name":"elementCanvas"},"coreMigrationVersion":"7.13.5","id":"custom-element-3bc52277-ee01-4cdc-8d2d-f2db6ade1512","references":[],"sort":[1651599812857,237],"type":"canvas-element","updated_at":"2022-05-03T17:43:32.857Z","version":"Wzk1LDFd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.5\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"ced0a5ea-3ec2-4274-8431-6e76d85637f6\"},\"panelIndex\":\"ced0a5ea-3ec2-4274-8431-6e76d85637f6\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"6a3b206a-4ac3-4d67-b2b9-c6b543a11ea3\":{\"columns\":{\"f70668f8-ae97-4b64-867f-b0c9b77914ef\":{\"label\":\"Top values of speaker\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"speaker\",\"isBucketed\":true,\"params\":{\"size\":39,\"orderBy\":{\"type\":\"column\",\"columnId\":\"fbf256d9-cae7-4244-8504-b73a5666e917\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"fbf256d9-cae7-4244-8504-b73a5666e917\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"}},\"columnOrder\":[\"f70668f8-ae97-4b64-867f-b0c9b77914ef\",\"fbf256d9-cae7-4244-8504-b73a5666e917\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_horizontal_percentage_stacked\",\"layers\":[{\"layerId\":\"6a3b206a-4ac3-4d67-b2b9-c6b543a11ea3\",\"accessors\":[\"fbf256d9-cae7-4244-8504-b73a5666e917\"],\"position\":\"top\",\"seriesType\":\"bar_horizontal_percentage_stacked\",\"showGridlines\":false,\"splitAccessor\":\"f70668f8-ae97-4b64-867f-b0c9b77914ef\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"4e937b20-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"4e937b20-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-6a3b206a-4ac3-4d67-b2b9-c6b543a11ea3\"}]},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"2b9a2bad-d6aa-4d3b-a692-fd96c3fb0ac1\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"We_like_lens\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}}},{\"version\":\"7.13.5\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"e51c2a40-5849-47c9-9ae2-a6373d68154b\"},\"panelIndex\":\"e51c2a40-5849-47c9-9ae2-a6373d68154b\",\"embeddableConfig\":{\"savedVis\":{\"title\":\"\",\"description\":\"\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"play_name\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":788,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"4e937b20-619d-11eb-aebf-c306684b328d\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"3775f69f-ad03-476d-bbf8-99ab9628a55d\",\"triggers\":[\"VALUE_CLICK_TRIGGER\"],\"action\":{\"factoryId\":\"URL_DRILLDOWN\",\"name\":\"drilldown_timebased \",\"config\":{\"url\":{\"template\":\"http://localhost:5601/app/discover#/view/b3288100-ca2c-11eb-bf5e-3de94e83d4f0?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&_a=(columns:!(),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'43fcac20-ca27-11eb-bf5e-3de94e83d4f0',key:geo.dest,negate:!f,params:(query:US),type:phrase),query:(match_phrase:(geo.dest:US)))),index:'43fcac20-ca27-11eb-bf5e-3de94e83d4f0',interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))\"},\"openInNewTab\":true,\"encodeUrl\":true}}}]}}}}]","timeRestore":false,"title":"nontimebased_shakespeare_drilldown","version":1},"coreMigrationVersion":"7.13.5","id":"e9eb20f0-ca2a-11eb-bf5e-3de94e83d4f0","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"ced0a5ea-3ec2-4274-8431-6e76d85637f6:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"ced0a5ea-3ec2-4274-8431-6e76d85637f6:indexpattern-datasource-layer-6a3b206a-4ac3-4d67-b2b9-c6b543a11ea3","type":"index-pattern"},{"id":"08dec860-ca29-11eb-bf5e-3de94e83d4f0","name":"ced0a5ea-3ec2-4274-8431-6e76d85637f6:drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:2b9a2bad-d6aa-4d3b-a692-fd96c3fb0ac1:dashboardId","type":"dashboard"},{"id":"4e937b20-619d-11eb-aebf-c306684b328d","name":"e51c2a40-5849-47c9-9ae2-a6373d68154b:kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"07f48f70-ca29-11eb-bf5e-3de94e83d4f0","name":"tag-07f48f70-ca29-11eb-bf5e-3de94e83d4f0","type":"tag"}],"sort":[1651612119207,862],"type":"dashboard","updated_at":"2022-05-03T21:08:39.207Z","version":"WzQ1MywxXQ=="} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"2e80716f-c1b6-46f2-be2b-35db744b5031\"},\"panelIndex\":\"2e80716f-c1b6-46f2-be2b-35db744b5031\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsPie\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"26e2cf99-d931-4320-9e15-9dbc148f3534\":{\"columns\":{\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\":{\"label\":\"Top values of url.raw\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"url.raw\",\"isBucketed\":true,\"params\":{\"size\":20,\"orderBy\":{\"type\":\"column\",\"columnId\":\"beb72af1-239c-46d8-823b-b00d1e2ace43\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"beb72af1-239c-46d8-823b-b00d1e2ace43\":{\"label\":\"Unique count of geo.srcdest\",\"dataType\":\"number\",\"operationType\":\"unique_count\",\"scale\":\"ratio\",\"sourceField\":\"geo.srcdest\",\"isBucketed\":false}},\"columnOrder\":[\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"beb72af1-239c-46d8-823b-b00d1e2ace43\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"shape\":\"donut\",\"layers\":[{\"layerId\":\"26e2cf99-d931-4320-9e15-9dbc148f3534\",\"groups\":[\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\",\"6adde1a2-4c6f-47eb-95cc-5c6a9d863a6e\"],\"metric\":\"beb72af1-239c-46d8-823b-b00d1e2ace43\",\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"default\",\"nestedLegend\":false}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-26e2cf99-d931-4320-9e15-9dbc148f3534\"}]},\"enhancements\":{},\"type\":\"lens\"},\"panelRefName\":\"panel_2e80716f-c1b6-46f2-be2b-35db744b5031\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"da8843e0-6789-4aae-bcd0-81f270538719\"},\"panelIndex\":\"da8843e0-6789-4aae-bcd0-81f270538719\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_da8843e0-6789-4aae-bcd0-81f270538719\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"adcd4418-7299-4efa-b369-5f71a7b4ebe0\"},\"panelIndex\":\"adcd4418-7299-4efa-b369-5f71a7b4ebe0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_adcd4418-7299-4efa-b369-5f71a7b4ebe0\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"869754a7-edf0-478f-a7f1-80374f63108a\"},\"panelIndex\":\"869754a7-edf0-478f-a7f1-80374f63108a\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_869754a7-edf0-478f-a7f1-80374f63108a\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":30,\"w\":24,\"h\":15,\"i\":\"67111cf4-338e-453f-8621-e8dea64082d1\"},\"panelIndex\":\"67111cf4-338e-453f-8621-e8dea64082d1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_67111cf4-338e-453f-8621-e8dea64082d1\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\"},\"panelIndex\":\"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":45,\"w\":24,\"h\":15,\"i\":\"88847944-ae1b-45fd-b102-3b45f9bea04b\"},\"panelIndex\":\"88847944-ae1b-45fd-b102-3b45f9bea04b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_88847944-ae1b-45fd-b102-3b45f9bea04b\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":45,\"w\":24,\"h\":15,\"i\":\"5a7924c7-eac0-4573-9199-fecec5b82e9e\"},\"panelIndex\":\"5a7924c7-eac0-4573-9199-fecec5b82e9e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5a7924c7-eac0-4573-9199-fecec5b82e9e\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":60,\"w\":24,\"h\":15,\"i\":\"f8f49591-f071-4a96-b1ed-cd65daff5648\"},\"panelIndex\":\"f8f49591-f071-4a96-b1ed-cd65daff5648\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_f8f49591-f071-4a96-b1ed-cd65daff5648\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":60,\"w\":24,\"h\":15,\"i\":\"9f357f47-c2a0-421f-a456-9583c40837ab\"},\"panelIndex\":\"9f357f47-c2a0-421f-a456-9583c40837ab\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9f357f47-c2a0-421f-a456-9583c40837ab\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":75,\"w\":24,\"h\":15,\"i\":\"6cb383e9-1e80-44f9-80d5-7b8c585668db\"},\"panelIndex\":\"6cb383e9-1e80-44f9-80d5-7b8c585668db\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6cb383e9-1e80-44f9-80d5-7b8c585668db\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":75,\"w\":24,\"h\":15,\"i\":\"57f5f0bf-6610-4599-aad4-37484640b5e2\"},\"panelIndex\":\"57f5f0bf-6610-4599-aad4-37484640b5e2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_57f5f0bf-6610-4599-aad4-37484640b5e2\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":90,\"w\":24,\"h\":15,\"i\":\"32d3ab66-52e1-44e3-8c1f-1dccff3c5692\"},\"panelIndex\":\"32d3ab66-52e1-44e3-8c1f-1dccff3c5692\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_32d3ab66-52e1-44e3-8c1f-1dccff3c5692\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":90,\"w\":24,\"h\":15,\"i\":\"dd1718fd-74ee-4032-851b-db97e893825d\"},\"panelIndex\":\"dd1718fd-74ee-4032-851b-db97e893825d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_dd1718fd-74ee-4032-851b-db97e893825d\"},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":105,\"w\":24,\"h\":15,\"i\":\"98a556ee-078b-4e03-93a8-29996133cdcb\"},\"panelIndex\":\"98a556ee-078b-4e03-93a8-29996133cdcb\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"999a2d60-cb2a-451c-8d71-80d7e92e70fd\":{\"columns\":{\"ce9117a2-773c-474c-8fb1-18940cf58b38\":{\"label\":\"Top values of type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"type\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\"},\"orderDirection\":\"asc\",\"otherBucket\":true,\"missingBucket\":false}},\"a3d10552-e352-40d0-a156-e86112c0501a\":{\"label\":\"Top values of _type\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"_type\",\"isBucketed\":true,\"params\":{\"size\":3,\"orderBy\":{\"type\":\"column\",\"columnId\":\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"},\"9c5db2f3-9eb0-4667-9a74-3318301de251\":{\"label\":\"Sum of bytes\",\"dataType\":\"number\",\"operationType\":\"sum\",\"sourceField\":\"bytes\",\"isBucketed\":false,\"scale\":\"ratio\"}},\"columnOrder\":[\"ce9117a2-773c-474c-8fb1-18940cf58b38\",\"a3d10552-e352-40d0-a156-e86112c0501a\",\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\",\"9c5db2f3-9eb0-4667-9a74-3318301de251\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"999a2d60-cb2a-451c-8d71-80d7e92e70fd\",\"accessors\":[\"cf07d1f1-d3fd-41f7-812c-d8587ec75959\",\"9c5db2f3-9eb0-4667-9a74-3318301de251\"],\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"xAccessor\":\"ce9117a2-773c-474c-8fb1-18940cf58b38\",\"splitAccessor\":\"a3d10552-e352-40d0-a156-e86112c0501a\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"56b34100-619d-11eb-aebf-c306684b328d\",\"name\":\"indexpattern-datasource-layer-999a2d60-cb2a-451c-8d71-80d7e92e70fd\"}]},\"enhancements\":{},\"type\":\"lens\"}},{\"version\":\"7.13.1\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":105,\"w\":24,\"h\":15,\"i\":\"62a0f0b0-3589-4cef-807b-b1b4258b7a9b\"},\"panelIndex\":\"62a0f0b0-3589-4cef-807b-b1b4258b7a9b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_62a0f0b0-3589-4cef-807b-b1b4258b7a9b\"}]","refreshInterval":{"pause":true,"value":0},"timeFrom":"2015-09-20T01:56:56.132Z","timeRestore":true,"timeTo":"2015-09-21T11:18:20.471Z","title":"lens_dashboard_logstash","version":1},"coreMigrationVersion":"7.13.5","id":"f458b9f0-bd9e-11eb-9520-1b4c3ca6a781","migrationVersion":{"dashboard":"7.13.1"},"references":[{"id":"21905950-bd9f-11eb-9520-1b4c3ca6a781","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:panel_2e80716f-c1b6-46f2-be2b-35db744b5031","type":"lens"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"2e80716f-c1b6-46f2-be2b-35db744b5031:indexpattern-datasource-layer-26e2cf99-d931-4320-9e15-9dbc148f3534","type":"index-pattern"},{"id":"aa4b8da0-bd9f-11eb-9520-1b4c3ca6a781","name":"da8843e0-6789-4aae-bcd0-81f270538719:panel_da8843e0-6789-4aae-bcd0-81f270538719","type":"lens"},{"id":"2d3f1250-bd9f-11eb-9520-1b4c3ca6a781","name":"adcd4418-7299-4efa-b369-5f71a7b4ebe0:panel_adcd4418-7299-4efa-b369-5f71a7b4ebe0","type":"lens"},{"id":"edd5a560-bda4-11eb-9520-1b4c3ca6a781","name":"869754a7-edf0-478f-a7f1-80374f63108a:panel_869754a7-edf0-478f-a7f1-80374f63108a","type":"lens"},{"id":"2c25a450-bda5-11eb-9520-1b4c3ca6a781","name":"67111cf4-338e-453f-8621-e8dea64082d1:panel_67111cf4-338e-453f-8621-e8dea64082d1","type":"lens"},{"id":"e79116e0-bd9e-11eb-9520-1b4c3ca6a781","name":"13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d:panel_13f21ad2-9b2d-4aa2-a908-b62e1bdedc1d","type":"lens"},{"id":"974fb950-bda5-11eb-9520-1b4c3ca6a781","name":"88847944-ae1b-45fd-b102-3b45f9bea04b:panel_88847944-ae1b-45fd-b102-3b45f9bea04b","type":"lens"},{"id":"21905950-bd9f-11eb-9520-1b4c3ca6a781","name":"5a7924c7-eac0-4573-9199-fecec5b82e9e:panel_5a7924c7-eac0-4573-9199-fecec5b82e9e","type":"lens"},{"id":"51b63040-bda5-11eb-9520-1b4c3ca6a781","name":"f8f49591-f071-4a96-b1ed-cd65daff5648:panel_f8f49591-f071-4a96-b1ed-cd65daff5648","type":"lens"},{"id":"b00679c0-bda5-11eb-9520-1b4c3ca6a781","name":"9f357f47-c2a0-421f-a456-9583c40837ab:panel_9f357f47-c2a0-421f-a456-9583c40837ab","type":"lens"},{"id":"652ade10-bd9f-11eb-9520-1b4c3ca6a781","name":"6cb383e9-1e80-44f9-80d5-7b8c585668db:panel_6cb383e9-1e80-44f9-80d5-7b8c585668db","type":"lens"},{"id":"7f3b5fb0-be2f-11eb-9520-1b4c3ca6a781","name":"57f5f0bf-6610-4599-aad4-37484640b5e2:panel_57f5f0bf-6610-4599-aad4-37484640b5e2","type":"lens"},{"id":"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781","name":"32d3ab66-52e1-44e3-8c1f-1dccff3c5692:panel_32d3ab66-52e1-44e3-8c1f-1dccff3c5692","type":"lens"},{"id":"dd315430-be2f-11eb-9520-1b4c3ca6a781","name":"dd1718fd-74ee-4032-851b-db97e893825d:panel_dd1718fd-74ee-4032-851b-db97e893825d","type":"lens"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"98a556ee-078b-4e03-93a8-29996133cdcb:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"56b34100-619d-11eb-aebf-c306684b328d","name":"98a556ee-078b-4e03-93a8-29996133cdcb:indexpattern-datasource-layer-999a2d60-cb2a-451c-8d71-80d7e92e70fd","type":"index-pattern"},{"id":"0dbbf8b0-be3c-11eb-9520-1b4c3ca6a781","name":"62a0f0b0-3589-4cef-807b-b1b4258b7a9b:panel_62a0f0b0-3589-4cef-807b-b1b4258b7a9b","type":"lens"},{"id":"e6994960-bd9e-11eb-9520-1b4c3ca6a781","name":"tag-e6994960-bd9e-11eb-9520-1b4c3ca6a781","type":"tag"}],"sort":[1651599812857,258],"type":"dashboard","updated_at":"2022-05-03T17:43:32.857Z","version":"Wzk3LDFd"} +{"attributes":{"description":"this is a logstash saved query","filters":[],"query":{"language":"kuery","query":"extension.raw :\"gif\" and machine.os.raw :\"ios\" "},"timefilter":{"from":"2015-09-20T01:56:56.132Z","refreshInterval":{"pause":true,"value":0},"to":"2015-09-21T11:18:20.471Z"},"title":"logstash_saved_query"},"coreMigrationVersion":"7.13.5","id":"logstash_saved_query","references":[],"sort":[1651599812857,259],"type":"query","updated_at":"2022-05-03T17:43:32.857Z","version":"Wzk5LDFd"} +{"attributes":{"description":"Shakespeare query","filters":[],"query":{"language":"kuery","query":"speaker : \"OTHELLO\" and play_name :\"Othello\" "},"title":"shakespeare_current_query"},"coreMigrationVersion":"7.13.5","id":"shakespeare_current_query","references":[],"sort":[1651599812857,260],"type":"query","updated_at":"2022-05-03T17:43:32.857Z","version":"WzEwMSwxXQ=="} +{"attributes":{"@created":"2021-05-27T18:53:18.432Z","@timestamp":"2021-05-27T19:46:12.539Z","assets":{},"colors":["#37988d","#c19628","#b83c6f","#3f9939","#1785b0","#ca5f35","#45bdb0","#f2bc33","#e74b8b","#4fbf48","#1ea6dc","#fd7643","#72cec3","#f5cc5d","#ec77a8","#7acf74","#4cbce4","#fd986f","#a1ded7","#f8dd91","#f2a4c5","#a6dfa2","#86d2ed","#fdba9f","#000000","#444444","#777777","#BBBBBB","#FFFFFF","rgba(255,255,255,0)"],"css":".canvasPage {\n\n}","height":720,"isWriteable":true,"name":"logstash-canvas-workpad","page":1,"pages":[{"elements":[{"expression":"savedLens id=\"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781\" timerange={timerange from=\"now-15y\" to=\"now\"}\n| render","filter":null,"id":"element-56d2ba72-f227-4d04-9478-a1d6f0c7e601","position":{"angle":0,"height":300,"left":20,"parent":null,"top":20,"width":500}},{"expression":"savedVisualization id=\"0d8a8860-623a-11eb-aebf-c306684b328d\" timerange={timerange from=\"now-15y\" to=\"now\"}\n| render","filter":null,"id":"element-afbaa26e-10e7-47d4-bb41-b061dfdced2b","position":{"angle":0,"height":300,"left":527,"parent":null,"top":20,"width":500}}],"groups":[],"id":"page-0f9ef2da-2868-4c0b-9223-fd3c9e53d6c9","style":{"background":"#FFF"},"transition":{}},{"elements":[{"expression":"image dataurl=null mode=\"contain\"\n| render","id":"element-c5534ef7-68c4-46bc-b35a-9e43a7f118c3","position":{"angle":0,"height":107,"left":20,"parent":null,"top":20,"width":132}},{"expression":"filters\n| essql query=\"SELECT machine.os.raw FROM \\\"logstash-*\\\"\"\n| pointseries x=\"machine.os.raw\" y=\"size(machine.os.raw)\" color=\"machine.os.raw\" size=\"sum(machine.os.raw)\"\n| plot defaultStyle={seriesStyle points=5 fill=1}\n| render","id":"element-5f7a3312-0e77-471c-9b8f-f98cb38075fb","position":{"angle":0,"height":192,"left":221,"parent":null,"top":56,"width":451}},{"expression":"timefilterControl compact=true column=@timestamp\n| render","filter":"timefilter from=\"now-29y\" to=now column=@timestamp","id":"element-6e00dcf4-06fe-4bd9-9315-d32d9d3fac5f","position":{"angle":0,"height":50,"left":221,"parent":null,"top":-1,"width":500}},{"expression":"filters\n| esdocs index=\"logstash-*\" fields=\"@timestamp, response.raw\"\n| pointseries x=\"size(response.raw)\" y=\"@timestamp\" color=\"response.raw\"\n| plot\n| render","id":"element-20281fac-1c3a-4ee3-9132-44379fb60b74","position":{"angle":0,"height":262,"left":51,"parent":null,"top":304,"width":590}},{"expression":"filters\n| timelion query=\".es(index=logstash-*, metric=sum:bytes)\"\n| pointseries x=\"@timestamp\" y=\"sum(value)\"\n| plot defaultStyle={seriesStyle lines=3}\n| render","id":"element-337b0548-5d6d-44cd-a324-eb50d63c7bd0","position":{"angle":0,"height":309,"left":648,"parent":null,"top":290,"width":369}},{"expression":"savedLens id=\"bb9e5bb0-be2f-11eb-9520-1b4c3ca6a781\" timerange={timerange from=\"now-15y\" to=\"now\"}\n| render","filter":null,"id":"element-353e5583-0dbb-4a6b-bac7-3b2a6b305397","position":{"angle":0,"height":181.99999999999997,"left":855,"parent":"group-d2618a19-3982-414e-93df-b2cb165b7c7e","top":15.000000000000014,"width":76.961271102284}},{"expression":"savedVisualization id=\"0d8a8860-623a-11eb-aebf-c306684b328d\" timerange={timerange from=\"now-15y\" to=\"now\"}\n| render","filter":null,"id":"element-0e5501a6-9e87-42bc-b539-1e697e62051b","position":{"angle":0,"height":181.99999999999997,"left":933.038728897716,"parent":"group-d2618a19-3982-414e-93df-b2cb165b7c7e","top":15.000000000000014,"width":76.961271102284}}],"groups":[],"id":"page-59c3cf09-1811-4324-995b-7336c1c11ab8","style":{"background":"#FFF"},"transition":{}}],"variables":[],"width":1080},"coreMigrationVersion":"7.13.5","id":"workpad-f2024ca3-e366-447a-b3af-7db4400646ef","migrationVersion":{"canvas-workpad":"7.0.0"},"references":[],"sort":[1651599812857,261],"type":"canvas-workpad","updated_at":"2022-05-03T17:43:32.857Z","version":"WzEwMiwxXQ=="} +{"exportedCount":82,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file diff --git a/x-pack/test/functional/apps/saved_objects_management/import_saved_objects_between_versions.ts b/x-pack/test/functional/apps/saved_objects_management/import_saved_objects_between_versions.ts index 168a2fd8d8aec..8716b10150d79 100644 --- a/x-pack/test/functional/apps/saved_objects_management/import_saved_objects_between_versions.ts +++ b/x-pack/test/functional/apps/saved_objects_management/import_saved_objects_between_versions.ts @@ -56,7 +56,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.savedObjects.clickImportDone(); await PageObjects.savedObjects.waitTableIsLoaded(); const newObjectCount = await PageObjects.savedObjects.getExportCount(); - expect(newObjectCount - initialObjectCount).to.eql(86); + expect(newObjectCount - initialObjectCount).to.eql(82); // logstash by reference dashboard with drilldowns await PageObjects.common.navigateToApp('dashboard'); diff --git a/x-pack/test/functional/apps/saved_objects_management/index.ts b/x-pack/test/functional/apps/saved_objects_management/index.ts index 17cdae0707213..dc0dae5134f50 100644 --- a/x-pack/test/functional/apps/saved_objects_management/index.ts +++ b/x-pack/test/functional/apps/saved_objects_management/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function savedObjectsManagementApp({ loadTestFile }: FtrProviderContext) { describe('Saved objects management', function savedObjectsManagementAppTestSuite() { - this.tags(['ciGroup2', 'skipFirefox']); + this.tags('skipFirefox'); loadTestFile(require.resolve('./spaces_integration')); loadTestFile(require.resolve('./feature_controls/saved_objects_management_security')); diff --git a/x-pack/test/functional/apps/security/basic_license/index.ts b/x-pack/test/functional/apps/security/basic_license/index.ts index 771874f853de4..04e55abb9bac1 100644 --- a/x-pack/test/functional/apps/security/basic_license/index.ts +++ b/x-pack/test/functional/apps/security/basic_license/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - basic license', function () { - this.tags('ciGroup4'); - loadTestFile(require.resolve('./role_mappings')); }); } diff --git a/x-pack/test/functional/apps/security/config.ts b/x-pack/test/functional/apps/security/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/security/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/security/index.ts b/x-pack/test/functional/apps/security/index.ts index fc9caafbabb29..3260e61e67cbf 100644 --- a/x-pack/test/functional/apps/security/index.ts +++ b/x-pack/test/functional/apps/security/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app', function () { - this.tags('ciGroup7'); - loadTestFile(require.resolve('./security')); loadTestFile(require.resolve('./doc_level_security_roles')); loadTestFile(require.resolve('./management')); diff --git a/x-pack/test/functional/apps/snapshot_restore/config.ts b/x-pack/test/functional/apps/snapshot_restore/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/snapshot_restore/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/snapshot_restore/index.ts b/x-pack/test/functional/apps/snapshot_restore/index.ts index 95fc0b80c2c91..0eefd3884cd31 100644 --- a/x-pack/test/functional/apps/snapshot_restore/index.ts +++ b/x-pack/test/functional/apps/snapshot_restore/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Snapshots app', function () { - this.tags(['ciGroup4', 'skipCloud']); + this.tags('skipCloud'); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/spaces/config.ts b/x-pack/test/functional/apps/spaces/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/spaces/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/spaces/index.ts b/x-pack/test/functional/apps/spaces/index.ts index 780d355e1f5c6..c951609d6a33f 100644 --- a/x-pack/test/functional/apps/spaces/index.ts +++ b/x-pack/test/functional/apps/spaces/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function spacesApp({ loadTestFile }: FtrProviderContext) { describe('Spaces app', function spacesAppTestSuite() { - this.tags('ciGroup9'); - loadTestFile(require.resolve('./copy_saved_objects')); loadTestFile(require.resolve('./feature_controls/spaces_security')); loadTestFile(require.resolve('./spaces_selection')); diff --git a/x-pack/test/functional/apps/status_page/config.ts b/x-pack/test/functional/apps/status_page/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/status_page/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/status_page/index.ts b/x-pack/test/functional/apps/status_page/index.ts index 69b18984f0ade..cdf6bb52ee605 100644 --- a/x-pack/test/functional/apps/status_page/index.ts +++ b/x-pack/test/functional/apps/status_page/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function statusPage({ loadTestFile }: FtrProviderContext) { describe('Status page', function statusPageTestSuite() { - this.tags('ciGroup4'); - loadTestFile(require.resolve('./status_page')); }); } diff --git a/x-pack/test/functional/apps/transform/config.ts b/x-pack/test/functional/apps/transform/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/transform/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 181325e83b36b..0c5227ae2f472 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -16,7 +16,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const transform = getService('transform'); describe('transform', function () { - this.tags(['ciGroup21', 'transform']); + this.tags('transform'); before(async () => { await transform.securityCommon.createTransformRoles(); diff --git a/x-pack/test/functional/apps/upgrade_assistant/config.ts b/x-pack/test/functional/apps/upgrade_assistant/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/upgrade_assistant/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/upgrade_assistant/index.ts b/x-pack/test/functional/apps/upgrade_assistant/index.ts index d1ab46463e930..fcb9dd4ba7cca 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/index.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function upgradeCheckup({ loadTestFile }: FtrProviderContext) { describe('Upgrade Assistant', function upgradeAssistantTestSuite() { - this.tags('ciGroup4'); - loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./deprecation_pages')); loadTestFile(require.resolve('./overview_page')); diff --git a/x-pack/test/functional/apps/uptime/config.ts b/x-pack/test/functional/apps/uptime/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/uptime/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index cc0578602951a..99359ee126502 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -42,8 +42,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { const uptime = getService('uptime'); describe('Uptime app', function () { - this.tags('ciGroup10'); - beforeEach('delete settings', async () => { await deleteUptimeSettingsObject(server); }); diff --git a/x-pack/test/functional/apps/visualize/config.ts b/x-pack/test/functional/apps/visualize/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/visualize/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index 85b9a31e2d361..0bf6f6fad2a75 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -138,10 +138,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('allows saving via the saved query management component popover with no saved query loaded', async () => { await queryBar.setQuery('response:200'); + await queryBar.clickQuerySubmitButton(); + await testSubjects.click('showQueryBarMenu'); await savedQueryManagementComponent.saveNewQuery('foo', 'bar', true, false); + await PageObjects.header.waitUntilLoadingHasFinished(); await savedQueryManagementComponent.savedQueryExistOrFail('foo'); await savedQueryManagementComponent.closeSavedQueryManagementComponent(); - + await testSubjects.click('showQueryBarMenu'); await savedQueryManagementComponent.deleteSavedQuery('foo'); await savedQueryManagementComponent.savedQueryMissingOrFail('foo'); }); @@ -170,13 +173,17 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('allow saving currently loaded query as a copy', async () => { await savedQueryManagementComponent.loadSavedQuery('OKJpgs'); + await queryBar.setQuery('response:404'); await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( 'ok2', 'description', true, false ); + await PageObjects.header.waitUntilLoadingHasFinished(); await savedQueryManagementComponent.savedQueryExistOrFail('ok2'); + await savedQueryManagementComponent.closeSavedQueryManagementComponent(); + await testSubjects.click('showQueryBarMenu'); await savedQueryManagementComponent.deleteSavedQuery('ok2'); }); }); diff --git a/x-pack/test/functional/apps/visualize/index.ts b/x-pack/test/functional/apps/visualize/index.ts index 105303aa1b537..c99182201eb5d 100644 --- a/x-pack/test/functional/apps/visualize/index.ts +++ b/x-pack/test/functional/apps/visualize/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function visualize({ loadTestFile }: FtrProviderContext) { describe('Visualize', function visualizeTestSuite() { - this.tags(['ciGroup30', 'skipFirefox']); + this.tags('skipFirefox'); loadTestFile(require.resolve('./feature_controls/visualize_security')); loadTestFile(require.resolve('./feature_controls/visualize_spaces')); diff --git a/x-pack/test/functional/apps/watcher/config.ts b/x-pack/test/functional/apps/watcher/config.ts new file mode 100644 index 0000000000000..d0d07ff200281 --- /dev/null +++ b/x-pack/test/functional/apps/watcher/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/watcher/index.js b/x-pack/test/functional/apps/watcher/index.js index fb39fe4aa7b29..fab662b26e80c 100644 --- a/x-pack/test/functional/apps/watcher/index.js +++ b/x-pack/test/functional/apps/watcher/index.js @@ -7,7 +7,7 @@ export default function ({ loadTestFile }) { describe('watcher app', function () { - this.tags(['ciGroup28', 'includeFirefox']); + this.tags('includeFirefox'); loadTestFile(require.resolve('./watcher_test')); }); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.base.js similarity index 88% rename from x-pack/test/functional/config.js rename to x-pack/test/functional/config.base.js index 8d39f26d4569d..a4d73d40e2d4d 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.base.js @@ -24,52 +24,10 @@ export default async function ({ readConfigFile }) { require.resolve('../../../test/common/config.js') ); const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); return { - // list paths to the files that contain your plugins tests - testFiles: [ - resolve(__dirname, './apps/home'), - resolve(__dirname, './apps/advanced_settings'), - resolve(__dirname, './apps/canvas'), - resolve(__dirname, './apps/graph'), - resolve(__dirname, './apps/monitoring'), - resolve(__dirname, './apps/watcher'), - resolve(__dirname, './apps/dashboard'), - resolve(__dirname, './apps/discover'), - resolve(__dirname, './apps/security'), - resolve(__dirname, './apps/spaces'), - resolve(__dirname, './apps/logstash'), - resolve(__dirname, './apps/grok_debugger'), - resolve(__dirname, './apps/infra'), - resolve(__dirname, './apps/ml'), - resolve(__dirname, './apps/rollup_job'), - resolve(__dirname, './apps/maps'), - resolve(__dirname, './apps/status_page'), - resolve(__dirname, './apps/upgrade_assistant'), - resolve(__dirname, './apps/visualize'), - resolve(__dirname, './apps/uptime'), - resolve(__dirname, './apps/saved_objects_management'), - resolve(__dirname, './apps/dev_tools'), - resolve(__dirname, './apps/apm'), - resolve(__dirname, './apps/api_keys'), - resolve(__dirname, './apps/data_views'), - resolve(__dirname, './apps/index_management'), - resolve(__dirname, './apps/index_lifecycle_management'), - resolve(__dirname, './apps/ingest_pipelines'), - resolve(__dirname, './apps/snapshot_restore'), - resolve(__dirname, './apps/cross_cluster_replication'), - resolve(__dirname, './apps/remote_clusters'), - resolve(__dirname, './apps/transform'), - resolve(__dirname, './apps/reporting_management'), - resolve(__dirname, './apps/management'), - resolve(__dirname, './apps/lens'), // smokescreen tests cause flakiness in other tests - - // This license_management file must be last because it is destructive. - resolve(__dirname, './apps/license_management'), - ], - services, pageObjects, @@ -93,6 +51,7 @@ export default async function ({ readConfigFile }) { '--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"', '--xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled=true', '--savedObjects.maxImportPayloadBytes=10485760', // for OSS test management/_import_objects, + '--uiSettings.overrides.observability:enableNewSyntheticsView=true', // for OSS test management/_import_objects, ], }, uiSettings: { diff --git a/x-pack/test/functional/config.ccs.ts b/x-pack/test/functional/config.ccs.ts index e6e0da5293190..87bdb17e736bd 100644 --- a/x-pack/test/functional/config.ccs.ts +++ b/x-pack/test/functional/config.ccs.ts @@ -10,12 +10,12 @@ import { RemoteEsArchiverProvider } from './services/remote_es/remote_es_archive import { RemoteEsProvider } from './services/remote_es/remote_es'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('./config')); + const functionalConfig = await readConfigFile(require.resolve('./config.base.js')); return { ...functionalConfig.getAll(), - testFiles: [require.resolve('./apps/lens')], + testFiles: [require.resolve('./apps/lens/group1')], junit: { reportName: 'X-Pack CCS Tests', diff --git a/x-pack/test/functional/config.coverage.js b/x-pack/test/functional/config.coverage.js deleted file mode 100644 index b403fe933c4a3..0000000000000 --- a/x-pack/test/functional/config.coverage.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export default async function ({ readConfigFile }) { - const chromeConfig = await readConfigFile(require.resolve('./config')); - - return { - ...chromeConfig.getAll(), - - suiteTags: { - exclude: ['skipCoverage'], - }, - - junit: { - reportName: 'Code Coverage for Functional Tests', - }, - }; -} diff --git a/x-pack/test/functional/config.edge.js b/x-pack/test/functional/config.edge.js index b51b09d0f4878..58f35e844e283 100644 --- a/x-pack/test/functional/config.edge.js +++ b/x-pack/test/functional/config.edge.js @@ -6,10 +6,10 @@ */ export default async function ({ readConfigFile }) { - const chromeConfig = await readConfigFile(require.resolve('./config')); + const firefoxConfig = await readConfigFile(require.resolve('./config.firefox.js')); return { - ...chromeConfig.getAll(), + ...firefoxConfig.getAll(), browser: { type: 'msedge', diff --git a/x-pack/test/functional/config.firefox.js b/x-pack/test/functional/config.firefox.js index 148f3c454a859..91249df789de8 100644 --- a/x-pack/test/functional/config.firefox.js +++ b/x-pack/test/functional/config.firefox.js @@ -6,16 +6,26 @@ */ export default async function ({ readConfigFile }) { - const chromeConfig = await readConfigFile(require.resolve('./config')); + const chromeConfig = await readConfigFile(require.resolve('./config.base.js')); return { ...chromeConfig.getAll(), + testFiles: [ + require.resolve('./apps/canvas'), + require.resolve('./apps/infra'), + require.resolve('./apps/security'), + require.resolve('./apps/spaces'), + require.resolve('./apps/status_page'), + require.resolve('./apps/watcher'), + ], + browser: { type: 'firefox', }, suiteTags: { + include: ['includeFirefox'], exclude: ['skipFirefox'], }, diff --git a/x-pack/test/functional/config_security_basic.ts b/x-pack/test/functional/config_security_basic.ts index dc4bfc437347e..d56b91d45a63e 100644 --- a/x-pack/test/functional/config_security_basic.ts +++ b/x-pack/test/functional/config_security_basic.ts @@ -20,7 +20,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); return { diff --git a/x-pack/test/functional/es_archives/alerts/data.json b/x-pack/test/functional/es_archives/alerts/data.json index 1c096d9df9930..3ce8cddcb284d 100644 --- a/x-pack/test/functional/es_archives/alerts/data.json +++ b/x-pack/test/functional/es_archives/alerts/data.json @@ -891,6 +891,57 @@ } } +{ + "type": "doc", + "value": { + "id": "alert:776cb5c0-ad1e-11ec-ab9e-5f5932f4fad8", + "index": ".kibana_1", + "source": { + "alert": { + "name": "123", + "alertTypeId": ".es-query", + "consumer": "alerts", + "params": { + "esQuery": "{\n \"query\":{\n \"match_all\" : {}\n }\n}", + "size": 100, + "timeWindowSize": 5, + "timeWindowUnit": "m", + "threshold": [ + 1000 + ], + "thresholdComparator": ">", + "index": [ + "kibana_sample_data_ecommerce" + ], + "timeField": "order_date" + }, + "schedule": { + "interval": "1m" + }, + "enabled": true, + "actions": [ + ], + "throttle": null, + "apiKeyOwner": null, + "createdBy" : "elastic", + "updatedBy" : "elastic", + "createdAt": "2022-03-26T16:04:50.698Z", + "muteAll": false, + "mutedInstanceIds": [], + "scheduledTaskId": "776cb5c0-ad1e-11ec-ab9e-5f5932f4fad8", + "tags": [] + }, + "type": "alert", + "updated_at": "2022-03-26T16:05:55.957Z", + "migrationVersion": { + "alert": "8.0.1" + }, + "references": [ + ] + } + } +} + { "type":"doc", "value":{ @@ -989,4 +1040,4 @@ ] } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json index 284c6e9519cc1..9c8d233e02074 100644 --- a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json @@ -57,7 +57,7 @@ "source": { "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", - "kibana.alert.rule.rule_type_id": "siem.signals", + "kibana.alert.rule.rule_type_id": "siem.queryRule", "message": "hello world security", "kibana.alert.rule.consumer": "siem", "kibana.alert.workflow_status": "open", @@ -74,7 +74,7 @@ "source": { "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", - "kibana.alert.rule.rule_type_id": "siem.customRule", + "kibana.alert.rule.rule_type_id": "siem.queryRule", "message": "hello world security", "kibana.alert.rule.consumer": "siem", "kibana.alert.workflow_status": "open", @@ -90,7 +90,7 @@ "id": "space1securityalert", "source": { "@timestamp": "2020-12-16T15:16:18.570Z", - "kibana.alert.rule.rule_type_id": "siem.signals", + "kibana.alert.rule.rule_type_id": "siem.queryRule", "message": "hello world security", "kibana.alert.rule.consumer": "siem", "kibana.alert.workflow_status": "open", @@ -106,7 +106,7 @@ "id": "space2securityalert", "source": { "@timestamp": "2020-12-16T15:16:18.570Z", - "kibana.alert.rule.rule_type_id": "siem.signals", + "kibana.alert.rule.rule_type_id": "siem.queryRule", "message": "hello world security", "kibana.alert.rule.consumer": "siem", "kibana.alert.workflow_status": "open", diff --git a/x-pack/test/functional/es_archives/security_solution/legacy_actions/data.json b/x-pack/test/functional/es_archives/security_solution/legacy_actions/data.json new file mode 100644 index 0000000000000..25aa371e02144 --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/legacy_actions/data.json @@ -0,0 +1,1064 @@ +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "action:c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "source" : { + "action" : { + "actionTypeId" : ".email", + "name" : "test", + "isMissingSecrets" : false, + "config" : { + "hasAuth" : true, + "from" : "a@a.com", + "host" : "gmail", + "port" : 56, + "service" : "other", + "secure" : null + }, + "secrets" : "jCv0fZczCM+rlWXIkI6Qo/ZqasgxPhplokCaNeXJrJlJYjKeCgbZgiLAEue4bpq4GUDohfye8o1BlJCQi+3HJPb2Qp6lhbcvXn+C6wUzxj9ZitwY8ZTDo34e22TX7qHFs/1mVfO4stjZ6X86ufRZttjTNu4qjchfi3Jao7nWbw==" + }, + "type" : "action", + "references" : [ ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "action" : "8.0.0" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:07:27.777Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "action:207fa0e0-c04e-11ec-8a52-4fb92379525a", + "source" : { + "action" : { + "actionTypeId" : ".slack", + "name" : "Slack connector", + "isMissingSecrets" : false, + "config" : { }, + "secrets" : "WTpvAWxAnR1yHdFqQrmGGeOmqGzeIDQnjGexz7F3JdFVEH0CJkPbiyEk9SiiBr1OPZ3s0LWqphjNREjyxSTCTuYTKaEIQ1+mJFQzjIulekMZklQdon7kNh4hOVHW832IX1Lpx9f5cgAPQst2bqJCehz+3BeZOh17Hgrvb/0zzKRCR4cvlhYbOUNUCNR/d6YoEniwUK8pO9xcel9hle7XY6MNfBGiMX8T41O87lvgp9xDjch3LpwqzDjUIFs=" + }, + "type" : "action", + "references" : [ ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "action" : "8.0.0" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:07:27.777Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:9095ee90-b075-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ no actions", + "tags" : [ + "__internal_rule_id:2297be91-894c-4831-830f-b424a0ec84f0", + "__internal_immutable:false", + "auto_disabled_8.0" + ], + "alertTypeId" : "siem.queryRule", + "consumer" : "siem", + "params" : { + "author" : [ ], + "description" : "a", + "ruleId" : "2297be91-894c-4831-830f-b424a0ec84f0", + "falsePositives" : [ ], + "from" : "now-360s", + "immutable" : false, + "license" : "", + "outputIndex" : "", + "meta" : { + "from" : "1m", + "kibana_siem_app_url" : "https://actions.kb.us-central1.gcp.cloud.es.io:9243/app/security" + }, + "maxSignals" : 100, + "riskScore" : 21, + "riskScoreMapping" : [ ], + "severity" : "low", + "severityMapping" : [ ], + "threat" : [ ], + "to" : "now", + "references" : [ ], + "version" : 1, + "exceptionsList" : [ ], + "type" : "query", + "language" : "kuery", + "index" : [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "query" : "*:*", + "filters" : [ ] + }, + "schedule" : { + "interval" : "5m" + }, + "enabled" : true, + "actions" : [ ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:05:53.511Z", + "updatedAt" : "2022-03-30T22:05:53.511Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "ok", + "lastExecutionDate" : "2022-03-31T19:53:37.507Z", + "error" : null, + "lastDuration" : 2377 + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "scheduledTaskId" : null, + "legacyId" : "9095ee90-b075-11ec-bb3f-1f063f8e06cf" + }, + "type" : "alert", + "references" : [ ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-31T19:53:39.885Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:dc6595f0-b075-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ action - every rule run", + "tags" : [ + "__internal_rule_id:72a0d429-363b-4f70-905e-c6019a224d40", + "__internal_immutable:false", + "auto_disabled_8.0" + ], + "alertTypeId" : "siem.queryRule", + "consumer" : "siem", + "params" : { + "author" : [ ], + "description" : "a", + "ruleId" : "72a0d429-363b-4f70-905e-c6019a224d40", + "falsePositives" : [ ], + "from" : "now-360s", + "immutable" : false, + "license" : "", + "outputIndex" : "", + "meta" : { + "from" : "1m", + "kibana_siem_app_url" : "https://actions.kb.us-central1.gcp.cloud.es.io:9243/app/security" + }, + "maxSignals" : 100, + "riskScore" : 21, + "riskScoreMapping" : [ ], + "severity" : "low", + "severityMapping" : [ ], + "threat" : [ ], + "to" : "now", + "references" : [ ], + "version" : 1, + "exceptionsList" : [ ], + "type" : "query", + "language" : "kuery", + "index" : [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "query" : "*:* ", + "filters" : [ ] + }, + "schedule" : { + "interval" : "5m" + }, + "enabled" : true, + "actions" : [ + { + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Test Actions" + }, + "actionTypeId" : ".email", + "actionRef" : "action_0" + } + ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:08:00.172Z", + "updatedAt" : "2022-03-30T22:08:00.172Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "ok", + "lastExecutionDate" : "2022-03-31T19:55:43.502Z", + "error" : null, + "lastDuration" : 2700 + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "scheduledTaskId" : null, + "legacyId" : "dc6595f0-b075-11ec-bb3f-1f063f8e06cf" + }, + "type" : "alert", + "references" : [ + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "name" : "action_0", + "type" : "action" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-31T19:55:46.202Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:064e3160-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ action - every hour", + "tags" : [ + "__internal_rule_id:4c056b05-75ac-4209-be32-82100f771eb4", + "__internal_immutable:false", + "auto_disabled_8.0" + ], + "alertTypeId" : "siem.queryRule", + "consumer" : "siem", + "params" : { + "author" : [ ], + "description" : "a", + "ruleId" : "4c056b05-75ac-4209-be32-82100f771eb4", + "falsePositives" : [ ], + "from" : "now-360s", + "immutable" : false, + "license" : "", + "outputIndex" : "", + "meta" : { + "from" : "1m", + "kibana_siem_app_url" : "https://actions.kb.us-central1.gcp.cloud.es.io:9243/app/security" + }, + "maxSignals" : 100, + "riskScore" : 21, + "riskScoreMapping" : [ ], + "severity" : "low", + "severityMapping" : [ ], + "threat" : [ ], + "to" : "now", + "references" : [ ], + "version" : 1, + "exceptionsList" : [ ], + "type" : "query", + "language" : "kuery", + "index" : [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "query" : "*:*", + "filters" : [ ] + }, + "schedule" : { + "interval" : "5m" + }, + "enabled" : true, + "actions" : [ ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:09:10.261Z", + "updatedAt" : "2022-03-30T22:09:10.261Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "ok", + "lastExecutionDate" : "2022-03-31T19:56:43.499Z", + "error" : null, + "lastDuration" : 2815 + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "scheduledTaskId" : null, + "legacyId" : "064e3160-b076-11ec-bb3f-1f063f8e06cf" + }, + "type" : "alert", + "references" : [ ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-31T19:56:46.314Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:27639570-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ action - daily", + "tags" : [ + "__internal_rule_id:8e2c8550-f13f-4e21-be0c-92148d71a5f1", + "__internal_immutable:false", + "auto_disabled_8.0" + ], + "alertTypeId" : "siem.queryRule", + "consumer" : "siem", + "params" : { + "author" : [ ], + "description" : "a", + "ruleId" : "8e2c8550-f13f-4e21-be0c-92148d71a5f1", + "falsePositives" : [ ], + "from" : "now-360s", + "immutable" : false, + "license" : "", + "outputIndex" : "", + "meta" : { + "from" : "1m", + "kibana_siem_app_url" : "https://actions.kb.us-central1.gcp.cloud.es.io:9243/app/security" + }, + "maxSignals" : 100, + "riskScore" : 21, + "riskScoreMapping" : [ ], + "severity" : "low", + "severityMapping" : [ ], + "threat" : [ ], + "to" : "now", + "references" : [ ], + "version" : 1, + "exceptionsList" : [ ], + "type" : "query", + "language" : "kuery", + "index" : [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "query" : "*:*", + "filters" : [ ] + }, + "schedule" : { + "interval" : "5m" + }, + "enabled" : true, + "actions" : [ ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:10:06.358Z", + "updatedAt" : "2022-03-30T22:10:06.358Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "ok", + "lastExecutionDate" : "2022-03-31T19:58:13.480Z", + "error" : null, + "lastDuration" : 3023 + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "scheduledTaskId" : null, + "legacyId" : "27639570-b076-11ec-bb3f-1f063f8e06cf" + }, + "type" : "alert", + "references" : [ ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-31T19:58:16.503Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:61ec7a40-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ actions - weekly", + "tags" : [ + "__internal_rule_id:05fbdd2a-e802-420b-bdc3-95ae0acca454", + "__internal_immutable:false", + "auto_disabled_8.0" + ], + "alertTypeId" : "siem.queryRule", + "consumer" : "siem", + "params" : { + "author" : [ ], + "description" : "a", + "ruleId" : "05fbdd2a-e802-420b-bdc3-95ae0acca454", + "falsePositives" : [ ], + "from" : "now-360s", + "immutable" : false, + "license" : "", + "outputIndex" : "", + "meta" : { + "from" : "1m", + "kibana_siem_app_url" : "https://actions.kb.us-central1.gcp.cloud.es.io:9243/app/security" + }, + "maxSignals" : 100, + "riskScore" : 21, + "riskScoreMapping" : [ ], + "severity" : "low", + "severityMapping" : [ ], + "threat" : [ ], + "to" : "now", + "references" : [ ], + "version" : 1, + "exceptionsList" : [ ], + "type" : "query", + "language" : "kuery", + "index" : [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "query" : "*:*", + "filters" : [ ] + }, + "schedule" : { + "interval" : "5m" + }, + "enabled" : true, + "actions" : [ ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:11:44.516Z", + "updatedAt" : "2022-03-30T22:11:44.516Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "ok", + "lastExecutionDate" : "2022-03-31T19:54:22.442Z", + "error" : null, + "lastDuration" : 2612 + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "scheduledTaskId" : null, + "legacyId" : "61ec7a40-b076-11ec-bb3f-1f063f8e06cf" + }, + "type" : "alert", + "references" : [ ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-31T19:54:25.054Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:64492ef0-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ actions - weekly", + "tags" : [ + "__internal_rule_alert_id:61ec7a40-b076-11ec-bb3f-1f063f8e06cf" + ], + "alertTypeId" : "siem.notifications", + "consumer" : "siem", + "params" : { + "ruleAlertId" : "61ec7a40-b076-11ec-bb3f-1f063f8e06cf" + }, + "schedule" : { + "interval" : "7d" + }, + "enabled" : true, + "actions" : [ + { + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Test Actions" + }, + "actionTypeId" : ".email", + "actionRef" : "action_0" + } + ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:11:48.661Z", + "updatedAt" : "2022-03-30T22:11:48.661Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "ok", + "lastExecutionDate" : "2022-03-30T22:11:51.640Z", + "error" : null + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "scheduledTaskId" : null, + "legacyId" : "64492ef0-b076-11ec-bb3f-1f063f8e06cf" + }, + "type" : "alert", + "references" : [ + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "name" : "action_0", + "type" : "action" + }, + { + "id" : "61ec7a40-b076-11ec-bb3f-1f063f8e06cf", + "name" : "param:alert_0", + "type" : "alert" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:11:51.707Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:d42e8210-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ action - every hour", + "tags" : [ + "__internal_rule_alert_id:064e3160-b076-11ec-bb3f-1f063f8e06cf" + ], + "alertTypeId" : "siem.notifications", + "consumer" : "siem", + "params" : { + "ruleAlertId" : "064e3160-b076-11ec-bb3f-1f063f8e06cf" + }, + "schedule" : { + "interval" : "1h" + }, + "enabled" : true, + "actions" : [ + { + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Rule email" + }, + "actionTypeId" : ".email", + "actionRef" : "action_0" + }, + { + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts" + }, + "actionTypeId" : ".slack", + "actionRef" : "action_1" + } + ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:14:56.318Z", + "updatedAt" : "2022-03-30T22:15:12.135Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "pending", + "lastExecutionDate" : "2022-03-30T22:14:56.318Z", + "error" : null + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "legacyId" : "d42e8210-b076-11ec-bb3f-1f063f8e06cf" + }, + "type" : "alert", + "references" : [ + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "name" : "action_0", + "type" : "action" + }, + { + "id" : "207fa0e0-c04e-11ec-8a52-4fb92379525a", + "name" : "action_1", + "type" : "action" + }, + { + "id" : "064e3160-b076-11ec-bb3f-1f063f8e06cf", + "name" : "param:alert_0", + "type" : "alert" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:15:12.135Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "alert:29ba2fa0-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "alert" : { + "name" : "test w/ action - daily", + "tags" : [ + "__internal_rule_alert_id:27639570-b076-11ec-bb3f-1f063f8e06cf" + ], + "alertTypeId" : "siem.notifications", + "consumer" : "siem", + "params" : { + "ruleAlertId" : "27639570-b076-11ec-bb3f-1f063f8e06cf" + }, + "schedule" : { + "interval" : "1d" + }, + "enabled" : true, + "actions" : [ + { + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Test Actions" + }, + "actionTypeId" : ".email", + "actionRef" : "action_0" + } + ], + "throttle" : null, + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "1527796724", + "updatedBy" : "1527796724", + "createdAt" : "2022-03-30T22:10:10.435Z", + "updatedAt" : "2022-03-30T22:10:10.435Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "ok", + "lastExecutionDate" : "2022-04-01T22:10:15.478Z", + "error" : null, + "lastDuration" : 984 + }, + "meta" : { + "versionApiKeyLastmodified" : "7.15.2" + }, + "scheduledTaskId" : null, + "legacyId" : "29ba2fa0-b076-11ec-bb3f-1f063f8e06cf", + "monitoring" : { + "execution" : { + "history" : [ + { + "duration" : 111, + "success" : true, + "timestamp" : 1648764614215 + }, + { + "duration" : 984, + "success" : true, + "timestamp" : 1648851016462 + } + ], + "calculated_metrics" : { + "p99" : 984, + "success_ratio" : 1, + "p50" : 547.5, + "p95" : 984 + } + } + } + }, + "type" : "alert", + "references" : [ + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "name" : "action_0", + "type" : "action" + }, + { + "id" : "27639570-b076-11ec-bb3f-1f063f8e06cf", + "name" : "param:alert_0", + "type" : "alert" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "alert" : "8.0.1" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-04-01T22:10:16.467Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "siem-detection-engine-rule-actions:926668d0-b075-11ec-bb3f-1f063f8e06cf", + "source" : { + "siem-detection-engine-rule-actions" : { + "actions" : [ ], + "ruleThrottle" : "no_actions", + "alertThrottle" : null + }, + "type" : "siem-detection-engine-rule-actions", + "references" : [ + { + "id" : "9095ee90-b075-11ec-bb3f-1f063f8e06cf", + "type" : "alert", + "name" : "alert_0" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "siem-detection-engine-rule-actions" : "8.0.0" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:05:55.563Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "siem-detection-engine-rule-actions:dde13970-b075-11ec-bb3f-1f063f8e06cf", + "source" : { + "siem-detection-engine-rule-actions" : { + "actions" : [ + { + "actionRef" : "action_0", + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Test Actions" + }, + "action_type_id" : ".email" + } + ], + "ruleThrottle" : "rule", + "alertThrottle" : null + }, + "type" : "siem-detection-engine-rule-actions", + "references" : [ + { + "id" : "dc6595f0-b075-11ec-bb3f-1f063f8e06cf", + "type" : "alert", + "name" : "alert_0" + }, + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "type" : "action", + "name" : "action_0" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "siem-detection-engine-rule-actions" : "8.0.0" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:08:02.207Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "siem-detection-engine-rule-actions:07aa8d10-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "siem-detection-engine-rule-actions" : { + "actions" : [ + { + "actionRef" : "action_0", + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Rule email" + }, + "action_type_id" : ".email" + }, + { + "actionRef" : "action_1", + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts" + }, + "action_type_id" : ".slack" + } + ], + "ruleThrottle" : "1h", + "alertThrottle" : "1h" + }, + "type" : "siem-detection-engine-rule-actions", + "references" : [ + { + "id" : "064e3160-b076-11ec-bb3f-1f063f8e06cf", + "type" : "alert", + "name" : "alert_0" + }, + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "type" : "action", + "name" : "action_0" + }, + { + "id" : "207fa0e0-c04e-11ec-8a52-4fb92379525a", + "type" : "action", + "name" : "action_1" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "siem-detection-engine-rule-actions" : "8.0.0" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:09:12.300Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "siem-detection-engine-rule-actions:291ae260-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "siem-detection-engine-rule-actions" : { + "actions" : [ + { + "actionRef" : "action_0", + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Test Actions" + }, + "action_type_id" : ".email" + } + ], + "ruleThrottle" : "1d", + "alertThrottle" : "1d" + }, + "type" : "siem-detection-engine-rule-actions", + "references" : [ + { + "id" : "27639570-b076-11ec-bb3f-1f063f8e06cf", + "type" : "alert", + "name" : "alert_0" + }, + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "type" : "action", + "name" : "action_0" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "siem-detection-engine-rule-actions" : "8.0.0" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:10:08.399Z" + } + } +} + +{ + "type": "doc", + "value": { + "index" : ".kibana", + "id" : "siem-detection-engine-rule-actions:63aa2fd0-b076-11ec-bb3f-1f063f8e06cf", + "source" : { + "siem-detection-engine-rule-actions" : { + "actions" : [ + { + "actionRef" : "action_0", + "group" : "default", + "params" : { + "message" : "Rule {{context.rule.name}} generated {{state.signals_count}} alerts", + "to" : [ + "test@test.com" + ], + "subject" : "Test Actions" + }, + "action_type_id" : ".email" + } + ], + "ruleThrottle" : "7d", + "alertThrottle" : "7d" + }, + "type" : "siem-detection-engine-rule-actions", + "references" : [ + { + "id" : "61ec7a40-b076-11ec-bb3f-1f063f8e06cf", + "type" : "alert", + "name" : "alert_0" + }, + { + "id" : "c95cb100-b075-11ec-bb3f-1f063f8e06cf", + "type" : "action", + "name" : "action_0" + } + ], + "namespaces" : [ + "default" + ], + "migrationVersion" : { + "siem-detection-engine-rule-actions" : "8.0.0" + }, + "coreMigrationVersion" : "8.1.2", + "updated_at" : "2022-03-30T22:11:46.643Z" + } + } +} diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index d38264150cfa5..7432c5e066a3d 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -28,6 +28,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont 'visualize', 'dashboard', 'timeToVisualize', + 'unifiedSearch', ]); return logWrapper('lensPage', log, { @@ -56,6 +57,8 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont fromTime = fromTime || PageObjects.timePicker.defaultStartTime; toTime = toTime || PageObjects.timePicker.defaultEndTime; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + // give some time for the update button tooltip to close + await PageObjects.common.sleep(500); }, /** @@ -96,6 +99,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont return retry.try(async () => { await testSubjects.click(`visListingTitleLink-${title}`); await this.isLensPageOrFail(); + await PageObjects.unifiedSearch.closeTourPopoverByLocalStorage(); }); }, @@ -566,10 +570,11 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont // pressing Enter at this point may lead to auto-complete the queryInput with random stuff from the // dropdown which was not intended originally. // To close the Filter popover we need to move to the label input and then press Enter: - // solution is to press Tab 2 twice (first Tab will close the dropdown) instead of Enter to avoid + // solution is to press Tab 3 tims (first Tab will close the dropdown) instead of Enter to avoid // race condition with the dropdown await PageObjects.common.pressTabKey(); await PageObjects.common.pressTabKey(); + await PageObjects.common.pressTabKey(); // Now it is safe to press Enter as we're in the label input await PageObjects.common.pressEnterKey(); await PageObjects.common.sleep(1000); // give time for debounced components to rerender @@ -837,7 +842,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * Changes the index pattern in the data panel */ async switchDataPanelIndexPattern(name: string) { - await testSubjects.click('indexPattern-switch-link'); + await testSubjects.click('lns-dataView-switch-link'); await find.clickByCssSelector(`[title="${name}"]`); await PageObjects.header.waitUntilLoadingHasFinished(); }, @@ -855,7 +860,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * Returns the current index pattern of the data panel */ async getDataPanelIndexPattern() { - return await (await testSubjects.find('indexPattern-switch-link')).getAttribute('title'); + return await (await testSubjects.find('lns-dataView-switch-link')).getAttribute('title'); }, /** @@ -1128,6 +1133,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await PageObjects.dashboard.switchToEditMode(); } await dashboardAddPanel.clickCreateNewLink(); + await PageObjects.unifiedSearch.closeTourPopoverByLocalStorage(); await this.goToTimeRange(); await this.configureDimension({ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', @@ -1200,7 +1206,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, async clickAddField() { - await testSubjects.click('lnsIndexPatternActions'); + await testSubjects.click('lns-dataView-switch-link'); await testSubjects.existOrFail('indexPattern-add-field'); await testSubjects.click('indexPattern-add-field'); }, @@ -1371,9 +1377,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, async closeSettingsMenu() { - if (!(await this.settingsMenuOpen())) return; - - await testSubjects.click('lnsApp_settingsButton'); + if (await this.settingsMenuOpen()) { + await testSubjects.click('lnsApp_settingsButton'); + } }, async enableAutoApply() { diff --git a/x-pack/test/functional/services/ml/common_data_grid.ts b/x-pack/test/functional/services/ml/common_data_grid.ts index c48cf92107dab..444b01cce902f 100644 --- a/x-pack/test/functional/services/ml/common_data_grid.ts +++ b/x-pack/test/functional/services/ml/common_data_grid.ts @@ -8,8 +8,8 @@ import expect from '@kbn/expect'; import { chunk } from 'lodash'; import type { ProvidedType } from '@kbn/test'; +import { asyncForEachWithLimit } from '@kbn/std'; import type { FtrProviderContext } from '../../ftr_provider_context'; -import { asyncForEach } from '../../apps/ml/settings/common'; export interface SetValueOptions { clearWithKeyboard?: boolean; @@ -154,7 +154,7 @@ export function MachineLearningCommonDataGridProvider({ getService }: FtrProvide await find.byClassName('euiDataGrid__controlScroll') ).findAllByCssSelector('[role="switch"]'); - await asyncForEach(visibilityToggles, async (toggle) => { + await asyncForEachWithLimit(visibilityToggles, 1, async (toggle) => { const checked = (await toggle.getAttribute('aria-checked')) === 'true'; expect(checked).to.eql( expectedState, diff --git a/x-pack/test/functional/services/ml/dashboard_embeddables.ts b/x-pack/test/functional/services/ml/dashboard_embeddables.ts index 1eb4e25a4edd5..74268f74d19a2 100644 --- a/x-pack/test/functional/services/ml/dashboard_embeddables.ts +++ b/x-pack/test/functional/services/ml/dashboard_embeddables.ts @@ -117,7 +117,9 @@ export function MachineLearningDashboardEmbeddablesProvider( async selectDiscoverIndexPattern(indexPattern: string) { await retry.tryForTime(2 * 1000, async () => { await PageObjects.discover.selectIndexPattern(indexPattern); - const indexPatternTitle = await testSubjects.getVisibleText('indexPattern-switch-link'); + const indexPatternTitle = await testSubjects.getVisibleText( + 'discover-dataView-switch-link' + ); expect(indexPatternTitle).to.be(indexPattern); }); }, diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts index 88ef0fdf08c8d..77f1e34e67157 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts @@ -49,6 +49,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( const jobTypeAttribute = `mlAnalyticsCreation-${jobType}-option`; await testSubjects.click(jobTypeAttribute); await this.assertJobTypeSelection(jobTypeAttribute); + await headerPage.waitUntilLoadingHasFinished(); }, async assertAdvancedEditorSwitchExists() { @@ -127,29 +128,41 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( await testSubjects.existOrFail('mlAnalyticsCreationDataGridHistogramButton'); }, - async enableSourceDataPreviewHistogramCharts(expectedDefaultButtonState: boolean) { - await this.assertSourceDataPreviewHistogramChartButtonCheckState(expectedDefaultButtonState); - if (expectedDefaultButtonState === false) { + async enableSourceDataPreviewHistogramCharts(shouldBeEnabled: boolean) { + const isEnabled = await this.getSourceDataPreviewHistogramChartButtonCheckState(); + if (isEnabled !== shouldBeEnabled) { await testSubjects.click('mlAnalyticsCreationDataGridHistogramButton'); - await this.assertSourceDataPreviewHistogramChartButtonCheckState(true); + await this.assertSourceDataPreviewHistogramChartEnabled(shouldBeEnabled); } }, - async assertSourceDataPreviewHistogramChartButtonCheckState(expectedCheckState: boolean) { - const actualCheckState = + async assertSourceDataPreviewHistogramChartEnabled(shouldBeEnabled: boolean) { + const isEnabled = await this.getSourceDataPreviewHistogramChartButtonCheckState(); + expect(isEnabled).to.eql( + shouldBeEnabled, + `Source data preview histogram charts should be '${ + shouldBeEnabled ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async getSourceDataPreviewHistogramChartButtonCheckState(): Promise { + return ( (await testSubjects.getAttribute( 'mlAnalyticsCreationDataGridHistogramButton', 'aria-pressed' - )) === 'true'; - expect(actualCheckState).to.eql( - expectedCheckState, - `Chart histogram button check state should be '${expectedCheckState}' (got '${actualCheckState}')` + )) === 'true' ); }, + async scrollSourceDataPreviewIntoView() { + await testSubjects.scrollIntoView('mlAnalyticsCreationDataGrid loaded'); + }, + async assertSourceDataPreviewHistogramCharts( expectedHistogramCharts: Array<{ chartAvailable: boolean; id: string; legend: string }> ) { + await this.scrollSourceDataPreviewIntoView(); // For each chart, get the content of each header cell and assert // the legend text and column id and if the chart should be present or not. await retry.tryForTime(10000, async () => { @@ -178,6 +191,17 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( }); }, + async enableAndAssertSourceDataPreviewHistogramCharts( + expectedHistogramCharts: Array<{ chartAvailable: boolean; id: string; legend: string }> + ) { + await retry.tryForTime(20 * 1000, async () => { + // turn histogram charts off and on before checking + await this.enableSourceDataPreviewHistogramCharts(false); + await this.enableSourceDataPreviewHistogramCharts(true); + await this.assertSourceDataPreviewHistogramCharts(expectedHistogramCharts); + }); + }, + async assertIncludeFieldsSelectionExists() { await testSubjects.existOrFail('mlAnalyticsCreateJobWizardIncludesTable', { timeout: 8000 }); diff --git a/x-pack/test/functional/services/transform/discover.ts b/x-pack/test/functional/services/transform/discover.ts index a98f7e5ae9890..d96ab079043a0 100644 --- a/x-pack/test/functional/services/transform/discover.ts +++ b/x-pack/test/functional/services/transform/discover.ts @@ -28,7 +28,7 @@ export function TransformDiscoverProvider({ getService }: FtrProviderContext) { async assertNoResults(expectedDestinationIndex: string) { // Discover should use the destination index pattern const actualIndexPatternSwitchLinkText = await ( - await testSubjects.find('indexPattern-switch-link') + await testSubjects.find('discover-dataView-switch-link') ).getVisibleText(); expect(actualIndexPatternSwitchLinkText).to.eql( expectedDestinationIndex, diff --git a/x-pack/test/functional/services/transform/wizard.ts b/x-pack/test/functional/services/transform/wizard.ts index 3c75db1c4c366..bc28120400895 100644 --- a/x-pack/test/functional/services/transform/wizard.ts +++ b/x-pack/test/functional/services/transform/wizard.ts @@ -25,7 +25,7 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi const comboBox = getService('comboBox'); const retry = getService('retry'); const ml = getService('ml'); - const PageObjects = getPageObjects(['discover', 'timePicker']); + const PageObjects = getPageObjects(['discover', 'timePicker', 'unifiedSearch']); return { async clickNextButton() { @@ -911,6 +911,7 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi await testSubjects.click('transformWizardCardDiscover'); await PageObjects.discover.isDiscoverAppOnScreen(); }); + await PageObjects.unifiedSearch.closeTourPopoverByLocalStorage(); }, async setDiscoverTimeRange(fromTime: string, toTime: string) { diff --git a/x-pack/test/functional_basic/apps/ml/index.ts b/x-pack/test/functional_basic/apps/ml/index.ts index af2fdc8c45f29..0188aa0361d94 100644 --- a/x-pack/test/functional_basic/apps/ml/index.ts +++ b/x-pack/test/functional_basic/apps/ml/index.ts @@ -12,7 +12,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); describe('machine learning basic license', function () { - this.tags(['ciGroup14', 'skipFirefox', 'mlqa']); + this.tags(['skipFirefox', 'mlqa']); before(async () => { await ml.securityCommon.createMlRoles(); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts index b44c5f08bdbc6..b90b97ca87a57 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts @@ -5,8 +5,6 @@ * 2.0. */ -import path from 'path'; - import { FtrProviderContext } from '../../../ftr_provider_context'; import { USER } from '../../../../functional/services/ml/security_common'; @@ -26,18 +24,8 @@ export default function ({ getService }: FtrProviderContext) { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - const uploadFilePath = path.join( - __dirname, - '..', - '..', - '..', - '..', - 'functional', - 'apps', - 'ml', - 'data_visualizer', - 'files_to_import', - 'artificial_server_log' + const uploadFilePath = require.resolve( + '../../../../functional/apps/ml/data_visualizer/files_to_import/artificial_server_log' ); const expectedUploadFileTitle = 'artificial_server_log'; diff --git a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts index c1b13d6dc1f11..fc293defceb86 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts @@ -5,8 +5,6 @@ * 2.0. */ -import path from 'path'; - import { FtrProviderContext } from '../../../ftr_provider_context'; import { USER } from '../../../../functional/services/ml/security_common'; @@ -26,18 +24,8 @@ export default function ({ getService }: FtrProviderContext) { const ecIndexPattern = 'ft_module_sample_ecommerce'; const ecExpectedTotalCount = '287'; - const uploadFilePath = path.join( - __dirname, - '..', - '..', - '..', - '..', - 'functional', - 'apps', - 'ml', - 'data_visualizer', - 'files_to_import', - 'artificial_server_log' + const uploadFilePath = require.resolve( + '../../../../functional/apps/ml/data_visualizer/files_to_import/artificial_server_log' ); const expectedUploadFileTitle = 'artificial_server_log'; diff --git a/x-pack/test/functional_basic/config.ts b/x-pack/test/functional_basic/config.ts index e1dac88436e4c..f35ece0ce5d16 100644 --- a/x-pack/test/functional_basic/config.ts +++ b/x-pack/test/functional_basic/config.ts @@ -8,7 +8,9 @@ import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); return { // default to the xpack functional config diff --git a/x-pack/test/functional_cors/config.ts b/x-pack/test/functional_cors/config.ts index 738285b4ff40f..364be1383ae94 100644 --- a/x-pack/test/functional_cors/config.ts +++ b/x-pack/test/functional_cors/config.ts @@ -16,7 +16,9 @@ const pluginPort = process.env.TEST_CORS_SERVER_PORT : 5699; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const kibanaFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const kibanaFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); const corsTestPlugin = Path.resolve(__dirname, './plugins/kibana_cors_test'); diff --git a/x-pack/test/functional_cors/tests/index.ts b/x-pack/test/functional_cors/tests/index.ts index 424dac86c4f1a..3ca455eccd339 100644 --- a/x-pack/test/functional_cors/tests/index.ts +++ b/x-pack/test/functional_cors/tests/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Kibana cors', function () { - this.tags('ciGroup12'); loadTestFile(require.resolve('./cors')); }); } diff --git a/x-pack/test/functional_embedded/config.ts b/x-pack/test/functional_embedded/config.ts index 868d53ee17ee9..cdbbffea0eab9 100644 --- a/x-pack/test/functional_embedded/config.ts +++ b/x-pack/test/functional_embedded/config.ts @@ -12,7 +12,9 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { pageObjects } from '../functional/page_objects'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const kibanaFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const kibanaFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); const iframeEmbeddedPlugin = resolve(__dirname, './plugins/iframe_embedded'); diff --git a/x-pack/test/functional_embedded/tests/index.ts b/x-pack/test/functional_embedded/tests/index.ts index aa210aff19728..1c3f01febd6d4 100644 --- a/x-pack/test/functional_embedded/tests/index.ts +++ b/x-pack/test/functional_embedded/tests/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Kibana embedded', function () { - this.tags('ciGroup2'); loadTestFile(require.resolve('./iframe_embedded')); }); } diff --git a/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/index.ts b/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/index.ts index 653f0842ef1bb..dda2e20745394 100644 --- a/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/index.ts +++ b/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Enterprise Search', function () { - this.tags('ciGroup10'); - loadTestFile(require.resolve('./app_search/setup_guide')); loadTestFile(require.resolve('./workplace_search/setup_guide')); }); diff --git a/x-pack/test/functional_enterprise_search/base_config.ts b/x-pack/test/functional_enterprise_search/base_config.ts index 2c21ccf5c5c39..8f72e1ebd6503 100644 --- a/x-pack/test/functional_enterprise_search/base_config.ts +++ b/x-pack/test/functional_enterprise_search/base_config.ts @@ -10,7 +10,9 @@ import { pageObjects } from './page_objects'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xPackFunctionalConfig = await readConfigFile(require.resolve('../functional/config')); + const xPackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); return { // default to the xpack functional config diff --git a/x-pack/test/functional_execution_context/config.ts b/x-pack/test/functional_execution_context/config.ts index 6169df6adcef3..caf88769b6a06 100644 --- a/x-pack/test/functional_execution_context/config.ts +++ b/x-pack/test/functional_execution_context/config.ts @@ -12,7 +12,7 @@ import { logFilePath } from './test_utils'; const alertTestPlugin = Path.resolve(__dirname, './fixtures/plugins/alerts'); export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); const servers = { ...functionalConfig.get('servers'), diff --git a/x-pack/test/functional_execution_context/tests/index.ts b/x-pack/test/functional_execution_context/tests/index.ts index c092be9bd8bdb..2c34783a9bae3 100644 --- a/x-pack/test/functional_execution_context/tests/index.ts +++ b/x-pack/test/functional_execution_context/tests/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Execution context', function () { - this.tags('ciGroup1'); loadTestFile(require.resolve('./browser')); loadTestFile(require.resolve('./server')); loadTestFile(require.resolve('./log_correlation')); diff --git a/x-pack/test/functional_synthetics/apps/uptime/index.ts b/x-pack/test/functional_synthetics/apps/uptime/index.ts index 925731c769bfc..64a9da5c30ea3 100644 --- a/x-pack/test/functional_synthetics/apps/uptime/index.ts +++ b/x-pack/test/functional_synthetics/apps/uptime/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile, getService }: FtrProviderContext) => { describe('Uptime app', function () { - this.tags('ciGroup8'); describe('with generated data', () => { loadTestFile(require.resolve('./synthetics_integration')); }); diff --git a/x-pack/test/functional_synthetics/config.js b/x-pack/test/functional_synthetics/config.js index 20488e3c52b2c..f5db09a5d60d9 100644 --- a/x-pack/test/functional_synthetics/config.js +++ b/x-pack/test/functional_synthetics/config.js @@ -28,7 +28,7 @@ export default async function ({ readConfigFile }) { require.resolve('../../../test/common/config.js') ); const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); // mount the config file for the package registry as well as diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/index.ts b/x-pack/test/functional_with_es_ssl/apps/cases/index.ts index 53d2c2d9767f1..04cceba32c8a7 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Cases', function () { - this.tags('ciGroup27'); loadTestFile(require.resolve('./create_case_form')); loadTestFile(require.resolve('./view_case')); loadTestFile(require.resolve('./list_view')); diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/index.ts b/x-pack/test/functional_with_es_ssl/apps/discover/index.ts index 708da2f02da74..f876140dbc92a 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/index.ts @@ -8,7 +8,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile, getService }: FtrProviderContext) => { describe('Discover alerting', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./search_source_alert')); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts index 9a486d4983d9c..4676004d9eb89 100644 --- a/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/ml/alert_flyout.ts @@ -68,8 +68,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { let testJobId = ''; describe('anomaly detection alert', function () { - this.tags('ciGroup13'); - before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index 49a3fada3dbef..a036c25e3d657 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -32,6 +32,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } describe('rules list', function () { + const assertRulesLength = async (length: number) => { + return await retry.try(async () => { + const rules = await pageObjects.triggersActionsUI.getAlertsList(); + expect(rules.length).to.equal(length); + }); + }; + before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('rulesTab'); @@ -603,13 +610,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should filter alerts by the rule status', async () => { - const assertRulesLength = async (length: number) => { - return await retry.try(async () => { - const rules = await pageObjects.triggersActionsUI.getAlertsList(); - expect(rules.length).to.equal(length); - }); - }; - // Enabled alert await createAlert({ supertest, @@ -639,25 +639,94 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Select enabled await testSubjects.click('ruleStatusFilterButton'); await testSubjects.click('ruleStatusFilterOption-enabled'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); await assertRulesLength(1); // Select disabled await testSubjects.click('ruleStatusFilterOption-enabled'); await testSubjects.click('ruleStatusFilterOption-disabled'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); await assertRulesLength(1); // Select snoozed await testSubjects.click('ruleStatusFilterOption-disabled'); await testSubjects.click('ruleStatusFilterOption-snoozed'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); await assertRulesLength(1); // Select disabled and snoozed await testSubjects.click('ruleStatusFilterOption-disabled'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); await assertRulesLength(2); // Select all 3 await testSubjects.click('ruleStatusFilterOption-enabled'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); await assertRulesLength(3); }); + + it('should filter alerts by the tag', async () => { + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['a'], + }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['b'], + }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['a', 'b'], + }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['b', 'c'], + }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['c'], + }, + }); + + await refreshAlertsList(); + await testSubjects.click('ruleTagFilter'); + + // Select a -> selected: a + await testSubjects.click('ruleTagFilterOption-a'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(2); + + // Unselect a -> selected: none + await testSubjects.click('ruleTagFilterOption-a'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(5); + + // Select a, b -> selected: a, b + await testSubjects.click('ruleTagFilterOption-a'); + await testSubjects.click('ruleTagFilterOption-b'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(4); + + // Unselect a, b, select c -> selected: c + await testSubjects.click('ruleTagFilterOption-a'); + await testSubjects.click('ruleTagFilterOption-b'); + await testSubjects.click('ruleTagFilterOption-c'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(2); + }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_table.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_table.ts index 436770696dbab..56026093c88dd 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_table.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_table.ts @@ -87,41 +87,48 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ); }); - it('should open a flyout and paginate through the flyout', async () => { - await PageObjects.common.navigateToUrlWithBrowserHistory('triggersActions', '/alerts'); - await waitTableIsLoaded(); - await testSubjects.click('expandColumnCellOpenFlyoutButton-0'); - await waitFlyoutOpen(); - - expect(await testSubjects.getVisibleText('alertsFlyoutTitle')).to.be( - 'APM Failed Transaction Rate (one)' - ); - expect(await testSubjects.getVisibleText('alertsFlyoutReason')).to.be( - 'Failed transactions rate is greater than 5.0% (current value is 31%) for elastic-co-frontend' - ); - - await testSubjects.click('alertsFlyoutPaginateNext'); - - expect(await testSubjects.getVisibleText('alertsFlyoutTitle')).to.be( - 'APM Failed Transaction Rate (one)' - ); - expect(await testSubjects.getVisibleText('alertsFlyoutReason')).to.be( - 'Failed transactions rate is greater than 5.0% (current value is 35%) for opbeans-python' - ); - - await testSubjects.click('alertsFlyoutPaginatePrevious'); - await testSubjects.click('alertsFlyoutPaginatePrevious'); - - await waitTableIsLoaded(); - - const rows = await getRows(); - expect(rows[0].status).to.be('close'); - expect(rows[0].lastUpdated).to.be('2021-10-19T14:55:14.503Z'); - expect(rows[0].duration).to.be('252002000'); - expect(rows[0].reason).to.be( - 'CPU usage is greater than a threshold of 40 (current value is 56.7%) for gke-edge-oblt-default-pool-350b44de-c3dd' - ); - }); + // This keeps failing in CI because the next button is not clickable + // Revisit this once we change the UI around based on feedback + /* + fail: Actions and Triggers app Alerts table should open a flyout and paginate through the flyout + │ Error: retry.try timeout: ElementClickInterceptedError: element click intercepted: Element ... is not clickable at point (1564, 795). Other element would receive the click:
...
+ */ + // it('should open a flyout and paginate through the flyout', async () => { + // await PageObjects.common.navigateToUrlWithBrowserHistory('triggersActions', '/alerts'); + // await waitTableIsLoaded(); + // await testSubjects.click('expandColumnCellOpenFlyoutButton-0'); + // await waitFlyoutOpen(); + // await waitFlyoutIsLoaded(); + + // expect(await testSubjects.getVisibleText('alertsFlyoutName')).to.be( + // 'APM Failed Transaction Rate (one)' + // ); + // expect(await testSubjects.getVisibleText('alertsFlyoutReason')).to.be( + // 'Failed transactions rate is greater than 5.0% (current value is 31%) for elastic-co-frontend' + // ); + + // await testSubjects.click('pagination-button-next'); + + // expect(await testSubjects.getVisibleText('alertsFlyoutName')).to.be( + // 'APM Failed Transaction Rate (one)' + // ); + // expect(await testSubjects.getVisibleText('alertsFlyoutReason')).to.be( + // 'Failed transactions rate is greater than 5.0% (current value is 35%) for opbeans-python' + // ); + + // await testSubjects.click('pagination-button-previous'); + // await testSubjects.click('pagination-button-previous'); + + // await waitTableIsLoaded(); + + // const rows = await getRows(); + // expect(rows[0].status).to.be('close'); + // expect(rows[0].lastUpdated).to.be('2021-10-19T14:55:14.503Z'); + // expect(rows[0].duration).to.be('252002000'); + // expect(rows[0].reason).to.be( + // 'CPU usage is greater than a threshold of 40 (current value is 56.7%) for gke-edge-oblt-default-pool-350b44de-c3dd' + // ); + // }); async function waitTableIsLoaded() { return await retry.try(async () => { @@ -130,12 +137,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); } - async function waitFlyoutOpen() { - return await retry.try(async () => { - const exists = await testSubjects.exists('alertsFlyout'); - if (!exists) throw new Error('Still loading...'); - }); - } + // async function waitFlyoutOpen() { + // return await retry.try(async () => { + // const exists = await testSubjects.exists('alertsFlyout'); + // if (!exists) throw new Error('Still loading...'); + // }); + // } + + // async function waitFlyoutIsLoaded() { + // return await retry.try(async () => { + // const exists = await testSubjects.exists('alertsFlyoutLoading'); + // if (exists) throw new Error('Still loading...'); + // }); + // } async function getRows() { const euiDataGridRows = await find.allByCssSelector('.euiDataGridRow'); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 1127a7423a3aa..56dfa17ef6268 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -168,6 +168,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const ruleType = await pageObjects.ruleDetailsUI.getRuleType(); expect(ruleType).to.be(`Always Firing`); + const owner = await pageObjects.ruleDetailsUI.getAPIKeyOwner(); + expect(owner).to.be('elastic'); + const { connectorType } = await pageObjects.ruleDetailsUI.getActionsLabels(); expect(connectorType).to.be(`Slack`); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts index 9c57f29c6f707..3b2803e17e184 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile, getService }: FtrProviderContext) => { describe('Actions and Triggers app', function () { - this.tags('ciGroup10'); loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./alerts_list')); loadTestFile(require.resolve('./alert_create_flyout')); @@ -17,6 +16,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./connectors')); loadTestFile(require.resolve('./alerts_table')); loadTestFile(require.resolve('./rule_status_dropdown')); + loadTestFile(require.resolve('./rule_tag_filter')); loadTestFile(require.resolve('./rule_status_filter')); loadTestFile(require.resolve('./rule_tag_badge')); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts new file mode 100644 index 0000000000000..77d57e2819db5 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const PageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); + const esArchiver = getService('esArchiver'); + + describe('Rule tag filter', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); + await PageObjects.common.navigateToUrlWithBrowserHistory( + 'triggersActions', + '/__components_sandbox' + ); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); + }); + + it('shoud load from shareable lazy loader', async () => { + await testSubjects.find('ruleTagFilter'); + const exists = await testSubjects.exists('ruleTagFilter'); + expect(exists).to.be(true); + }); + + it('should allow tag filters to be selected', async () => { + let badge = await find.byCssSelector('.euiFilterButton__notification'); + expect(await badge.getVisibleText()).to.be('0'); + + await testSubjects.click('ruleTagFilter'); + await testSubjects.click('ruleTagFilterOption-tag1'); + + badge = await find.byCssSelector('.euiFilterButton__notification'); + expect(await badge.getVisibleText()).to.be('1'); + + await testSubjects.click('ruleTagFilterOption-tag2'); + + badge = await find.byCssSelector('.euiFilterButton__notification'); + expect(await badge.getVisibleText()).to.be('2'); + + await testSubjects.click('ruleTagFilterOption-tag1'); + expect(await badge.getVisibleText()).to.be('1'); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts index d2078267bde85..2c39ef045972f 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/index.ts @@ -14,8 +14,6 @@ export default ({ getService, loadTestFile }: FtrProviderContext) => { const kibanaServer = getService('kibanaServer'); describe('Uptime app', function () { - this.tags('ciGroup6'); - describe('with real-world data', () => { before(async () => { await esArchiver.load(ARCHIVE); diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 4783ad683c0cf..62984ace526fb 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -31,7 +31,9 @@ const enabledActionTypes = [ ]; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); const servers = { ...xpackFunctionalConfig.get('servers'), @@ -74,6 +76,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.trigger_actions_ui.enableExperimental=${JSON.stringify([ 'internalAlertsTable', 'internalShareableComponentsSandbox', + 'ruleTagFilter', 'ruleStatusFilter', ])}`, `--xpack.alerting.rules.minimumScheduleInterval.value="2s"`, diff --git a/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts index 01d7c24be2f41..cff396276eefd 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/rule_details.ts @@ -21,6 +21,9 @@ export function RuleDetailsPageProvider({ getService }: FtrProviderContext) { async getRuleType() { return await testSubjects.getVisibleText('ruleTypeLabel'); }, + async getAPIKeyOwner() { + return await testSubjects.getVisibleText('apiKeyOwnerLabel'); + }, async getActionsLabels() { return { connectorType: await testSubjects.getVisibleText('actionTypeLabel'), diff --git a/x-pack/test/licensing_plugin/config.ts b/x-pack/test/licensing_plugin/config.ts index 155d761020b29..c4b197c10a824 100644 --- a/x-pack/test/licensing_plugin/config.ts +++ b/x-pack/test/licensing_plugin/config.ts @@ -11,7 +11,9 @@ import { services, pageObjects } from './services'; const license = 'basic'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalTestsConfig = await readConfigFile(require.resolve('../functional/config.js')); + const functionalTestsConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); const servers = { ...functionalTestsConfig.get('servers'), diff --git a/x-pack/test/licensing_plugin/public/index.ts b/x-pack/test/licensing_plugin/public/index.ts index 194db6266b510..904b9eaecd757 100644 --- a/x-pack/test/licensing_plugin/public/index.ts +++ b/x-pack/test/licensing_plugin/public/index.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../services'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile }: FtrProviderContext) { describe('Licensing plugin public client', function () { - this.tags('ciGroup5'); loadTestFile(require.resolve('./feature_usage')); // MUST BE LAST! CHANGES LICENSE TYPE! loadTestFile(require.resolve('./updates')); diff --git a/x-pack/test/licensing_plugin/server/index.ts b/x-pack/test/licensing_plugin/server/index.ts index 619d29a4a2fd2..28426eba962b8 100644 --- a/x-pack/test/licensing_plugin/server/index.ts +++ b/x-pack/test/licensing_plugin/server/index.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../services'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile }: FtrProviderContext) { describe('Licensing plugin server client', function () { - this.tags('ciGroup13'); loadTestFile(require.resolve('./info')); loadTestFile(require.resolve('./header')); diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts index c28447ef0ac18..95cce7e7fb85f 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('lists api security and spaces enabled', function () { - this.tags('ciGroup1'); - loadTestFile(require.resolve('./create_lists')); loadTestFile(require.resolve('./create_list_items')); loadTestFile(require.resolve('./read_lists')); diff --git a/x-pack/test/load/config.ts b/x-pack/test/load/config.ts index 2d20806f3e9e8..dcaa2031c9c02 100644 --- a/x-pack/test/load/config.ts +++ b/x-pack/test/load/config.ts @@ -21,7 +21,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') + require.resolve('../functional/config.base.js') ); return { diff --git a/x-pack/test/observability_api_integration/basic/tests/index.ts b/x-pack/test/observability_api_integration/basic/tests/index.ts index c62cf4be0d7c7..a1ea41e2b7c36 100644 --- a/x-pack/test/observability_api_integration/basic/tests/index.ts +++ b/x-pack/test/observability_api_integration/basic/tests/index.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function observabilityApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('Observability specs (basic)', function () { - this.tags('ciGroup1'); loadTestFile(require.resolve('./annotations')); }); } diff --git a/x-pack/test/observability_api_integration/trial/tests/index.ts b/x-pack/test/observability_api_integration/trial/tests/index.ts index 037cf48275de2..e426efd90188c 100644 --- a/x-pack/test/observability_api_integration/trial/tests/index.ts +++ b/x-pack/test/observability_api_integration/trial/tests/index.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('Observability specs (trial)', function () { - this.tags('ciGroup1'); loadTestFile(require.resolve('./annotations')); }); } diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts index 9ec3791aef35f..d60f93f1285ad 100644 --- a/x-pack/test/observability_functional/apps/observability/index.ts +++ b/x-pack/test/observability_functional/apps/observability/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('ObservabilityApp', function () { - this.tags('ciGroup22'); - loadTestFile(require.resolve('./alerts')); loadTestFile(require.resolve('./alerts/add_to_case')); loadTestFile(require.resolve('./alerts/alert_disclaimer')); diff --git a/x-pack/test/observability_functional/with_rac_write.config.ts b/x-pack/test/observability_functional/with_rac_write.config.ts index 71a1de1df6a77..bc5b39358fedb 100644 --- a/x-pack/test/observability_functional/with_rac_write.config.ts +++ b/x-pack/test/observability_functional/with_rac_write.config.ts @@ -27,7 +27,9 @@ const enabledActionTypes = [ ]; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); const servers = { ...xpackFunctionalConfig.get('servers'), diff --git a/x-pack/test/osquery_cypress/config.ts b/x-pack/test/osquery_cypress/config.ts index 2bd39acfa1359..37f7b3f63b36c 100644 --- a/x-pack/test/osquery_cypress/config.ts +++ b/x-pack/test/osquery_cypress/config.ts @@ -14,7 +14,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') + require.resolve('../functional/config.base.js') ); return { diff --git a/x-pack/test/performance/config.playwright.ts b/x-pack/test/performance/config.playwright.ts index 9077c58a30e15..44a53d7be80a1 100644 --- a/x-pack/test/performance/config.playwright.ts +++ b/x-pack/test/performance/config.playwright.ts @@ -15,7 +15,7 @@ const APM_SERVER_URL = 'https://kibana-ops-e2e-perf.apm.us-central1.gcp.cloud.es const APM_PUBLIC_TOKEN = 'CTs9y3cvcfq13bQqsB'; export default async function ({ readConfigFile, log }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); const testFiles = [require.resolve('./tests/playwright')]; @@ -63,6 +63,7 @@ export default async function ({ readConfigFile, log }: FtrConfigProviderContext performancePhase: process.env.TEST_PERFORMANCE_PHASE, journeyName: process.env.JOURNEY_NAME, testJobId, + testBuildId, }) .filter(([, v]) => !!v) .reduce((acc, [k, v]) => (acc ? `${acc},${k}=${v}` : `${k}=${v}`), ''), diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/index.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/index.ts index c29367fb852ab..0901c96f522cc 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/index.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('event_log', function taskManagerSuite() { - this.tags('ciGroup6'); loadTestFile(require.resolve('./public_api_integration')); loadTestFile(require.resolve('./service_api_integration')); }); diff --git a/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/index.ts b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/index.ts index 7f2a4c12a26eb..6ee46b58c4bcd 100644 --- a/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/index.ts +++ b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Licensed feature usage APIs', function () { - this.tags('ciGroup13'); loadTestFile(require.resolve('./feature_usage')); }); } diff --git a/x-pack/test/plugin_api_integration/test_suites/platform/index.ts b/x-pack/test/plugin_api_integration/test_suites/platform/index.ts index 46c468e9b6d78..907ebfe6bdf79 100644 --- a/x-pack/test/plugin_api_integration/test_suites/platform/index.ts +++ b/x-pack/test/plugin_api_integration/test_suites/platform/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('platform', function taskManagerSuite() { - this.tags('ciGroup13'); loadTestFile(require.resolve('./elasticsearch_client')); }); } diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts index fe494ac33d461..2712069008598 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('task_manager', function taskManagerSuite() { - this.tags('ciGroup12'); loadTestFile(require.resolve('./health_route')); loadTestFile(require.resolve('./task_management')); loadTestFile(require.resolve('./task_management_scheduled_at')); diff --git a/x-pack/test/plugin_api_perf/README.md b/x-pack/test/plugin_api_perf/README.md index f47a2aeb7878a..2ae7c7d201328 100644 --- a/x-pack/test/plugin_api_perf/README.md +++ b/x-pack/test/plugin_api_perf/README.md @@ -71,7 +71,7 @@ Ideally we can clean this up and make it easier and less hacky in the future, bu 1. You can run the FTS in the main clone of your fork by running `node scripts/functional_tests_server.js --config=test/plugin_api_perf/config.js` in the `x-pack` folder. 1. Once you've began running the default FTS, you want your second FTS to run such that it is referencing the Elasticsearch instance started by that first FTS. You achieve this by exporting a `TEST_ES_URL` Environment variable that points at it. By default, you should be able to run this: `export TEST_ES_URL=http://elastic:changeme@localhost:9220`. Do this in a terminal window opened in your **second** clone of Kibana (in my case, the `./elastic/_kibana` folder). 1. One issue I encountered with FTS is that I can't tell it _not to start its own ES instance at all_. To achieve this, in `packages/kbn-test/src/functional_tests/tasks.js` you need to comment out the line that starts up its own ES (`const es = await runElasticsearch({ config, options: opts });` [line 85] and `await es.cleanup();` shortly after) -1. Next you want each instance of Kibana to run with its own UUID as that is used to identify each Kibana's owned tasks. In the file `x-pack/test/functional/config.js` simple change the uuid on the line `--server.uuid=` into any random UUID. +1. Next you want each instance of Kibana to run with its own UUID as that is used to identify each Kibana's owned tasks. In the file `x-pack/test/functional/config.base.js` simple change the uuid on the line `--server.uuid=` into any random UUID. 1. Now that you've made these changes you can kick off your second Kibana FTS by running ths following in the second clone's `x-pack` folder: `TEST_KIBANA_PORT=5621 node scripts/functional_tests_server.js --config=test/plugin_api_perf/config.js`. This runs Kibana on a different port than the first FTS (`5621` instead of `5620`). 1. With two FTS Kibana running and both pointing at the same Elasticsearch. Now, you can run the actual perf test by running `node scripts/functional_test_runner.js --config=test/plugin_api_perf/config.js` in a third terminal diff --git a/x-pack/test/plugin_api_perf/test_suites/task_manager/index.ts b/x-pack/test/plugin_api_perf/test_suites/task_manager/index.ts index 703c97e4f2c63..9055a2d9e023c 100644 --- a/x-pack/test/plugin_api_perf/test_suites/task_manager/index.ts +++ b/x-pack/test/plugin_api_perf/test_suites/task_manager/index.ts @@ -15,7 +15,6 @@ export default function ({ loadTestFile }: { loadTestFile: (file: string) => voi * worth keeping around for future use, rather than being rewritten time and time again. */ describe.skip('task_manager_perf', function taskManagerSuite() { - this.tags('ciGroup12'); loadTestFile(require.resolve('./task_manager_perf_integration')); }); } diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts index 8f3c5be04a8bc..a21b8f406e506 100644 --- a/x-pack/test/plugin_functional/config.ts +++ b/x-pack/test/plugin_functional/config.ts @@ -17,7 +17,9 @@ import { pageObjects } from './page_objects'; // that returns an object with the projects config values export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); // Find all folders in ./plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(resolve(__dirname, 'plugins')); diff --git a/x-pack/test/plugin_functional/test_suites/global_search/index.ts b/x-pack/test/plugin_functional/test_suites/global_search/index.ts index a87d9c5e4d503..651bb2b903924 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/index.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('GlobalSearch API', function () { - this.tags('ciGroup7'); loadTestFile(require.resolve('./global_search_providers')); loadTestFile(require.resolve('./global_search_bar')); }); diff --git a/x-pack/test/plugin_functional/test_suites/resolver/index.ts b/x-pack/test/plugin_functional/test_suites/resolver/index.ts index 688ba536b1232..3c43683b6bf7a 100644 --- a/x-pack/test/plugin_functional/test_suites/resolver/index.ts +++ b/x-pack/test/plugin_functional/test_suites/resolver/index.ts @@ -29,8 +29,6 @@ export default function ({ // FLAKY: https://github.com/elastic/kibana/issues/87425 describe('Resolver test app', function () { - this.tags('ciGroup7'); - // Note: these tests are intended to run on the same page in serial. before(async function () { await pageObjects.common.navigateToApp('resolverTest'); diff --git a/x-pack/test/plugin_functional/test_suites/timelines/index.ts b/x-pack/test/plugin_functional/test_suites/timelines/index.ts index 2ca8d81132ab3..955966eab12c0 100644 --- a/x-pack/test/plugin_functional/test_suites/timelines/index.ts +++ b/x-pack/test/plugin_functional/test_suites/timelines/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Timelines plugin API', function () { - this.tags('ciGroup7'); const pageObjects = getPageObjects(['common']); const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts index 4cff15dc9f444..808c813145b84 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('Reporting APIs', function () { - this.tags('ciGroup2'); - before(async () => { const reportingAPI = getService('reportingAPI'); await reportingAPI.logTaskManagerHealth(); diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/index.ts b/x-pack/test/reporting_api_integration/reporting_without_security/index.ts index 72cfc36947517..19f96aa5d2869 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security/index.ts @@ -14,7 +14,7 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { const reportingAPI = getService('reportingAPI'); await reportingAPI.logTaskManagerHealth(); }); - this.tags('ciGroup13'); + loadTestFile(require.resolve('./job_apis_csv')); }); } diff --git a/x-pack/test/reporting_functional/reporting_and_deprecated_security/index.ts b/x-pack/test/reporting_functional/reporting_and_deprecated_security/index.ts index 4725cb1eae82e..722b6545115cd 100644 --- a/x-pack/test/reporting_functional/reporting_and_deprecated_security/index.ts +++ b/x-pack/test/reporting_functional/reporting_and_deprecated_security/index.ts @@ -43,8 +43,6 @@ export default function (context: FtrProviderContext) { }; describe('Reporting Functional Tests with Deprecated Security configuration enabled', function () { - this.tags('ciGroup20'); - before(async () => { const reportingAPI = context.getService('reportingAPI'); await reportingAPI.logTaskManagerHealth(); diff --git a/x-pack/test/reporting_functional/reporting_and_security.config.ts b/x-pack/test/reporting_functional/reporting_and_security.config.ts index 3037aeacde033..7d8c3ed696696 100644 --- a/x-pack/test/reporting_functional/reporting_and_security.config.ts +++ b/x-pack/test/reporting_functional/reporting_and_security.config.ts @@ -11,7 +11,7 @@ import { ReportingAPIProvider } from '../reporting_api_integration/services'; import { ReportingFunctionalProvider } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); // Reporting API tests need a fully working UI + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); // Reporting API tests need a fully working UI const apiConfig = await readConfigFile(require.resolve('../api_integration/config')); return { diff --git a/x-pack/test/reporting_functional/reporting_and_security/index.ts b/x-pack/test/reporting_functional/reporting_and_security/index.ts index 4b06eb426389e..ec7afea96f194 100644 --- a/x-pack/test/reporting_functional/reporting_and_security/index.ts +++ b/x-pack/test/reporting_functional/reporting_and_security/index.ts @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('Reporting Functional Tests with Security enabled', function () { - this.tags('ciGroup20'); - before(async () => { const reportingFunctional = getService('reportingFunctional'); await reportingFunctional.logTaskManagerHealth(); diff --git a/x-pack/test/reporting_functional/reporting_without_security/index.ts b/x-pack/test/reporting_functional/reporting_without_security/index.ts index fecc0e97daac0..cc07e97a9c3a7 100644 --- a/x-pack/test/reporting_functional/reporting_without_security/index.ts +++ b/x-pack/test/reporting_functional/reporting_without_security/index.ts @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile, getService }: FtrProviderContext) { describe('Reporting Functional Tests with Security disabled', function () { - this.tags('ciGroup2'); - before(async () => { const reportingAPI = getService('reportingAPI'); await reportingAPI.logTaskManagerHealth(); diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/find_alerts.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/find_alerts.ts index ef06fc80c485c..35461b47f6def 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/find_alerts.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/find_alerts.ts @@ -50,7 +50,6 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const TEST_URL = '/internal/rac/alerts'; - const ALERTS_INDEX_URL = `${TEST_URL}/index`; const SPACE1 = 'space1'; const SPACE2 = 'space2'; const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV'; @@ -58,38 +57,7 @@ export default ({ getService }: FtrProviderContext) => { const SECURITY_SOLUTION_ALERT_ID = '020202'; const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts'; - const getAPMIndexName = async (user: User) => { - const { body: indexNames }: { body: { index_name: string[] | undefined } } = - await supertestWithoutAuth - .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) - .auth(user.username, user.password) - .set('kbn-xsrf', 'true') - .expect(200); - const observabilityIndex = indexNames?.index_name?.find( - (indexName) => indexName === APM_ALERT_INDEX - ); - expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below - }; - - const getSecuritySolutionIndexName = async (user: User) => { - const { body: indexNames }: { body: { index_name: string[] | undefined } } = - await supertestWithoutAuth - .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) - .auth(user.username, user.password) - .set('kbn-xsrf', 'true') - .expect(200); - const securitySolution = indexNames?.index_name?.find((indexName) => - indexName.startsWith(SECURITY_SOLUTION_ALERT_INDEX) - ); - expect(securitySolution).to.eql(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`); // assert this here so we can use constants in the dynamically-defined test cases below - }; - describe('Alert - Find - RBAC - spaces', () => { - before(async () => { - await getSecuritySolutionIndexName(superUser); - await getAPMIndexName(superUser); - }); - before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); }); @@ -98,6 +66,32 @@ export default ({ getService }: FtrProviderContext) => { await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts'); }); + it(`${superUser.username} should reject at route level when aggs contains script alerts which match query in ${SPACE1}/${SECURITY_SOLUTION_ALERT_INDEX}`, async () => { + const found = await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}/find`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .send({ + query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } }, + aggs: { + alertsByGroupingCount: { + terms: { + field: 'kibana.alert.rule.name', + order: { + _count: 'desc', + }, + script: { + source: 'SCRIPT', + }, + size: 10000, + }, + }, + }, + index: SECURITY_SOLUTION_ALERT_INDEX, + }); + expect(found.statusCode).to.eql(400); + }); + it(`${superUser.username} should reject at route level when nested aggs contains script alerts which match query in ${SPACE1}/${SECURITY_SOLUTION_ALERT_INDEX}`, async () => { const found = await supertestWithoutAuth .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}/find`) @@ -164,6 +158,26 @@ export default ({ getService }: FtrProviderContext) => { expect(found.body.hits.total.value).to.be.above(0); }); + it(`${superUser.username} should allow cardinality aggs in ${SPACE1}/${SECURITY_SOLUTION_ALERT_INDEX}`, async () => { + const found = await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}/find`) + .auth(superUser.username, superUser.password) + .set('kbn-xsrf', 'true') + .send({ + size: 1, + aggs: { + nbr_consumer: { + cardinality: { + field: 'kibana.alert.rule.consumer', + }, + }, + }, + index: '.alerts*', + }); + expect(found.statusCode).to.eql(200); + expect(found.body.aggregations.nbr_consumer.value).to.be.equal(2); + }); + function addTests({ space, authorizedUsers, unauthorizedUsers, alertId, index }: TestCase) { authorizedUsers.forEach(({ username, password }) => { it(`${username} should finds alerts which match query in ${space}/${index}`, async () => { diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts index 1ba48d5d1aa27..3b30f1b64dc2b 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/get_alerts_index.ts @@ -20,17 +20,16 @@ export default ({ getService }: FtrProviderContext) => { const TEST_URL = '/internal/rac/alerts'; const ALERTS_INDEX_URL = `${TEST_URL}/index`; const SPACE1 = 'space1'; - const APM_ALERT_INDEX = '.alerts-observability-apm'; + const APM_ALERT_INDEX = '.alerts-observability.apm.alerts'; const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts'; - const getAPMIndexName = async (user: User, space: string, expected: number = 200) => { - const { body: indexNames }: { body: { index_name: string[] | undefined } } = - await supertestWithoutAuth - .get(`${getSpaceUrlPrefix(space)}${ALERTS_INDEX_URL}?features=apm`) - .auth(user.username, user.password) - .set('kbn-xsrf', 'true') - .expect(expected); - return indexNames; + const getAPMIndexName = async (user: User, space: string, expectedStatusCode: number = 200) => { + const resp = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${ALERTS_INDEX_URL}?features=apm`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(expectedStatusCode); + return resp.body.index_name as string[]; }; const getSecuritySolutionIndexName = async ( @@ -38,13 +37,13 @@ export default ({ getService }: FtrProviderContext) => { space: string, expectedStatusCode: number = 200 ) => { - const { body: indexNames }: { body: { index_name: string[] | undefined } } = - await supertestWithoutAuth - .get(`${getSpaceUrlPrefix(space)}${ALERTS_INDEX_URL}?features=siem`) - .auth(user.username, user.password) - .set('kbn-xsrf', 'true') - .expect(expectedStatusCode); - return indexNames; + const resp = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${ALERTS_INDEX_URL}?features=siem`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .expect(expectedStatusCode); + + return resp.body.index_name as string[]; }; describe('Alert - Get Index - RBAC - spaces', () => { @@ -54,31 +53,22 @@ export default ({ getService }: FtrProviderContext) => { describe('Users:', () => { it(`${obsOnlySpacesAll.username} should be able to access the APM alert in ${SPACE1}`, async () => { const indexNames = await getAPMIndexName(obsOnlySpacesAll, SPACE1); - const observabilityIndex = indexNames?.index_name?.find( - (indexName) => indexName === APM_ALERT_INDEX - ); - expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + expect(indexNames.includes(`${APM_ALERT_INDEX}-${SPACE1}`)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below }); it(`${superUser.username} should be able to access the APM alert in ${SPACE1}`, async () => { const indexNames = await getAPMIndexName(superUser, SPACE1); - const observabilityIndex = indexNames?.index_name?.find( - (indexName) => indexName === APM_ALERT_INDEX - ); - expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + expect(indexNames.includes(`${APM_ALERT_INDEX}-${SPACE1}`)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below }); it(`${secOnlyRead.username} should NOT be able to access the APM alert in ${SPACE1}`, async () => { const indexNames = await getAPMIndexName(secOnlyRead, SPACE1); - expect(indexNames?.index_name?.length).to.eql(0); + expect(indexNames?.length).to.eql(0); }); it(`${secOnlyRead.username} should be able to access the security solution alert in ${SPACE1}`, async () => { const indexNames = await getSecuritySolutionIndexName(secOnlyRead, SPACE1); - const securitySolution = indexNames?.index_name?.find((indexName) => - indexName.startsWith(SECURITY_SOLUTION_ALERT_INDEX) - ); - expect(securitySolution).to.eql(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`); // assert this here so we can use constants in the dynamically-defined test cases below + expect(indexNames.includes(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`)).to.eql(true); // assert this here so we can use constants in the dynamically-defined test cases below }); }); }); diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts index 229f31375200a..d010cbfce9150 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/index.ts @@ -11,9 +11,6 @@ import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/ // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('rules security and spaces enabled: basic', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - before(async () => { await createSpacesAndUsers(getService); }); @@ -27,8 +24,9 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { // loadTestFile(require.resolve('./get_alert_by_id')); // loadTestFile(require.resolve('./update_alert')); // loadTestFile(require.resolve('./bulk_update_alerts')); - // loadTestFile(require.resolve('./find_alerts')); - // loadTestFile(require.resolve('./get_alerts_index')); + + loadTestFile(require.resolve('./get_alerts_index')); + loadTestFile(require.resolve('./find_alerts')); loadTestFile(require.resolve('./search_strategy')); }); }; diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts index 27dfd7a9c9a90..dfda18b5a0c05 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts @@ -19,7 +19,7 @@ import { waitForSignalsToBePresent, waitForRuleSuccessOrStatus, } from '../../../../detection_engine_api_integration/utils'; -import { ID } from '../../../../detection_engine_api_integration/security_and_spaces/tests/generating_signals'; +import { ID } from '../../../../detection_engine_api_integration/security_and_spaces/group1/generating_signals'; import { obsOnlySpacesAllEsRead, obsOnlySpacesAll, @@ -43,7 +43,7 @@ export default ({ getService }: FtrProviderContext) => { const SPACE1 = 'space1'; // Failing: See https://github.com/elastic/kibana/issues/129219 - describe.skip('ruleRegistryAlertsSearchStrategy', () => { + describe('ruleRegistryAlertsSearchStrategy', () => { let kibanaVersion: string; before(async () => { kibanaVersion = await kbnClient.version.get(); diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/trial/index.ts b/x-pack/test/rule_registry/security_and_spaces/tests/trial/index.ts index 3e13d64b936a4..53a788f6c7829 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/trial/index.ts @@ -39,9 +39,6 @@ import { export default ({ loadTestFile, getService }: FtrProviderContext): void => { // FAILING: https://github.com/elastic/kibana/issues/110153 describe.skip('rules security and spaces enabled: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - before(async () => { await createSpaces(getService); await createUsersAndRoles( diff --git a/x-pack/test/rule_registry/spaces_only/tests/basic/index.ts b/x-pack/test/rule_registry/spaces_only/tests/basic/index.ts index aeb2b085ad379..f47b4b6254ff2 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/basic/index.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/basic/index.ts @@ -11,9 +11,6 @@ import { createSpaces, deleteSpaces } from '../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('rule registry spaces only: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - before(async () => { await createSpaces(getService); }); diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts index a9969fd3bfd52..d248858f19f6d 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/get_alert_by_id.ts @@ -31,10 +31,9 @@ export default ({ getService }: FtrProviderContext) => { .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) .set('kbn-xsrf', 'true') .expect(200); - const observabilityIndex = indexNames?.index_name?.find( - (indexName) => indexName === APM_ALERT_INDEX - ); - expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + expect( + indexNames?.index_name?.filter((indexName) => indexName.startsWith(APM_ALERT_INDEX)).length + ).to.eql(1); // assert this here so we can use constants in the dynamically-defined test cases below }; const getSecuritySolutionIndexName = async (user: User) => { @@ -43,10 +42,11 @@ export default ({ getService }: FtrProviderContext) => { .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) .set('kbn-xsrf', 'true') .expect(200); - const securitySolution = indexNames?.index_name?.find((indexName) => - indexName.startsWith(SECURITY_SOLUTION_ALERT_INDEX) - ); - expect(securitySolution).to.eql(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`); // assert this here so we can use constants in the dynamically-defined test cases below + expect( + indexNames?.index_name?.filter((indexName) => + indexName.startsWith(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`) + ).length + ).to.eql(1); // assert this here so we can use constants in the dynamically-defined test cases below }; describe('Alerts - GET - RBAC', () => { diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts index 19e35019eb50a..d519dd16dab45 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts @@ -11,9 +11,6 @@ import { createSpaces, deleteSpaces } from '../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('rule registry spaces only: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - before(async () => { await createSpaces(getService); }); diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/update_alert.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/update_alert.ts index 5b5bc64a5a9bd..31c3cfdecb9ee 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/update_alert.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/update_alert.ts @@ -31,10 +31,9 @@ export default ({ getService }: FtrProviderContext) => { .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) .set('kbn-xsrf', 'true') .expect(200); - const observabilityIndex = indexNames?.index_name?.find( - (indexName) => indexName === APM_ALERT_INDEX - ); - expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below + expect( + indexNames?.index_name?.filter((indexName) => indexName.startsWith(APM_ALERT_INDEX)).length + ).to.eql(1); // assert this here so we can use constants in the dynamically-defined test cases below }; const getSecuritySolutionIndexName = async (user: User) => { @@ -43,10 +42,11 @@ export default ({ getService }: FtrProviderContext) => { .get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`) .set('kbn-xsrf', 'true') .expect(200); - const securitySolution = indexNames?.index_name?.find((indexName) => - indexName.startsWith(SECURITY_SOLUTION_ALERT_INDEX) - ); - expect(securitySolution).to.eql(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`); // assert this here so we can use constants in the dynamically-defined test cases below + expect( + indexNames?.index_name?.filter((indexName) => + indexName.startsWith(`${SECURITY_SOLUTION_ALERT_INDEX}-${SPACE1}`) + ).length + ).to.eql(1); // assert this here so we can use constants in the dynamically-defined test cases below }; describe('Alert - Update - RBAC - spaces', () => { diff --git a/x-pack/test/saved_object_api_integration/common/config.ts b/x-pack/test/saved_object_api_integration/common/config.ts index 8ca74c7fcea49..32d2d73d5cebc 100644 --- a/x-pack/test/saved_object_api_integration/common/config.ts +++ b/x-pack/test/saved_object_api_integration/common/config.ts @@ -24,7 +24,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) const config = { kibana: { api: await readConfigFile(path.resolve(REPO_ROOT, 'test/api_integration/config.js')), - functional: await readConfigFile(require.resolve('../../../../test/functional/config.js')), + functional: await readConfigFile( + require.resolve('../../../../test/functional/config.base.js') + ), }, xpack: { api: await readConfigFile(require.resolve('../../api_integration/config.ts')), diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts index 740b9d91927bf..4eb0b90480314 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts @@ -13,8 +13,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const supertest = getService('supertest'); describe('saved objects security and spaces enabled', function () { - this.tags('ciGroup20'); - before(async () => { await createUsersAndRoles(es, supertest); }); diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts index c6bdbde07fc02..1be7ed754a971 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('saved objects spaces only enabled', function () { - this.tags('ciGroup5'); - loadTestFile(require.resolve('./bulk_create')); loadTestFile(require.resolve('./bulk_get')); loadTestFile(require.resolve('./bulk_resolve')); diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts index 54740e73aba65..f28b3cd615887 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts @@ -11,8 +11,6 @@ import { createUsersAndRoles } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('saved objects tagging API - security and spaces integration', function () { - this.tags('ciGroup10'); - before(async () => { await createUsersAndRoles(getService); }); diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts index 3d5b0b9c3b989..f291d2537ed02 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/index.ts @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../services'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile }: FtrProviderContext) { describe('saved objects tagging API', function () { - this.tags('ciGroup12'); - loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./update')); diff --git a/x-pack/test/saved_object_tagging/functional/config.ts b/x-pack/test/saved_object_tagging/functional/config.ts index 6ad1f05e2be5b..1c40864f2fa02 100644 --- a/x-pack/test/saved_object_tagging/functional/config.ts +++ b/x-pack/test/saved_object_tagging/functional/config.ts @@ -11,7 +11,7 @@ import { services, pageObjects } from './ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../functional/config.js') + require.resolve('../../functional/config.base.js') ); return { diff --git a/x-pack/test/saved_object_tagging/functional/tests/index.ts b/x-pack/test/saved_object_tagging/functional/tests/index.ts index fbf0954382dd1..2d79d0a7a45ec 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/index.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/index.ts @@ -11,8 +11,6 @@ import { createUsersAndRoles } from '../../common/lib'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile, getService }: FtrProviderContext) { describe('saved objects tagging - functional tests', function () { - this.tags('ciGroup14'); - before(async () => { await createUsersAndRoles(getService); }); diff --git a/x-pack/test/saved_objects_field_count/config.ts b/x-pack/test/saved_objects_field_count/config.ts index 7967b6c4f3b9c..ab5154adb8d59 100644 --- a/x-pack/test/saved_objects_field_count/config.ts +++ b/x-pack/test/saved_objects_field_count/config.ts @@ -6,7 +6,6 @@ */ import { FtrConfigProviderContext } from '@kbn/test'; -import { testRunner } from './runner'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaCommonTestsConfig = await readConfigFile( @@ -16,7 +15,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...kibanaCommonTestsConfig.getAll(), - testRunner, + testFiles: [require.resolve('./test')], esTestCluster: { license: 'trial', @@ -28,5 +27,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...kibanaCommonTestsConfig.get('kbnTestServer'), serverArgs: [...kibanaCommonTestsConfig.get('kbnTestServer.serverArgs')], }, + + junit: { + reportName: 'Saved Object Field Count', + }, }; } diff --git a/x-pack/test/saved_objects_field_count/runner.ts b/x-pack/test/saved_objects_field_count/runner.ts deleted file mode 100644 index b88f2129ba64d..0000000000000 --- a/x-pack/test/saved_objects_field_count/runner.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CiStatsReporter } from '@kbn/ci-stats-reporter'; -import { FtrProviderContext } from '../functional/ftr_provider_context'; - -const IGNORED_FIELDS = [ - // The following fields are returned by the _field_caps API but aren't counted - // towards the index field limit. - '_seq_no', - '_id', - '_version', - '_field_names', - '_ignored', - '_feature', - '_index', - '_routing', - '_source', - '_type', - '_nested_path', - '_timestamp', - // migrationVersion is dynamic so will be anywhere between 1..type count - // depending on which objects are present in the index when querying the - // field caps API. See https://github.com/elastic/kibana/issues/70815 - 'migrationVersion', -]; - -export async function testRunner({ getService }: FtrProviderContext) { - const log = getService('log'); - const es = getService('es'); - - const reporter = CiStatsReporter.fromEnv(log); - - log.debug('Saved Objects field count metrics starting'); - - const { fields } = await es.fieldCaps({ - index: '.kibana', - fields: '*', - }); - - const fieldCountPerTypeMap: Map = Object.keys(fields) - .map((f) => f.split('.')[0]) - .filter((f) => !IGNORED_FIELDS.includes(f)) - .reduce((accumulator, f) => { - accumulator.set(f, accumulator.get(f) + 1 || 1); - return accumulator; - }, new Map()); - - const metrics = Array.from(fieldCountPerTypeMap.entries()) - .sort((a, b) => a[0].localeCompare(b[0])) - .map(([fieldType, count]) => ({ - group: 'Saved Objects .kibana field count', - id: fieldType, - value: count, - })); - - log.debug( - 'Saved Objects field count metrics:\n', - metrics.map(({ id, value }) => `${id}:${value}`).join('\n') - ); - await reporter.metrics(metrics); - log.debug('Saved Objects field count metrics done'); -} diff --git a/x-pack/test/saved_objects_field_count/test.ts b/x-pack/test/saved_objects_field_count/test.ts new file mode 100644 index 0000000000000..e931b1aa5ef26 --- /dev/null +++ b/x-pack/test/saved_objects_field_count/test.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CiStatsReporter } from '@kbn/ci-stats-reporter'; +import { FtrProviderContext } from '../functional/ftr_provider_context'; + +const IGNORED_FIELDS = [ + // The following fields are returned by the _field_caps API but aren't counted + // towards the index field limit. + '_seq_no', + '_id', + '_version', + '_field_names', + '_ignored', + '_feature', + '_index', + '_routing', + '_source', + '_type', + '_nested_path', + '_timestamp', + // migrationVersion is dynamic so will be anywhere between 1..type count + // depending on which objects are present in the index when querying the + // field caps API. See https://github.com/elastic/kibana/issues/70815 + 'migrationVersion', +]; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const log = getService('log'); + const es = getService('es'); + + describe('Saved Objects Field Count', () => { + it('capture', async () => { + const reporter = CiStatsReporter.fromEnv(log); + + log.debug('Saved Objects field count metrics starting'); + + const { fields } = await es.fieldCaps({ + index: '.kibana', + fields: '*', + }); + + const fieldCountPerTypeMap: Map = Object.keys(fields) + .map((f) => f.split('.')[0]) + .filter((f) => !IGNORED_FIELDS.includes(f)) + .reduce((accumulator, f) => { + accumulator.set(f, accumulator.get(f) + 1 || 1); + return accumulator; + }, new Map()); + + const metrics = Array.from(fieldCountPerTypeMap.entries()) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([fieldType, count]) => ({ + group: 'Saved Objects .kibana field count', + id: fieldType, + value: count, + })); + + log.debug( + 'Saved Objects field count metrics:\n', + metrics.map(({ id, value }) => `${id}:${value}`).join('\n') + ); + + await reporter.metrics(metrics); + log.debug('Saved Objects field count metrics done'); + }); + }); +} diff --git a/x-pack/test/screenshot_creation/config.ts b/x-pack/test/screenshot_creation/config.ts index 659034e9fbe8b..18dda361ac2cd 100644 --- a/x-pack/test/screenshot_creation/config.ts +++ b/x-pack/test/screenshot_creation/config.ts @@ -9,7 +9,9 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); return { // default to the xpack functional config diff --git a/x-pack/test/search_sessions_integration/config.ts b/x-pack/test/search_sessions_integration/config.ts index 9dc542038a48a..2d570a607c746 100644 --- a/x-pack/test/search_sessions_integration/config.ts +++ b/x-pack/test/search_sessions_integration/config.ts @@ -10,7 +10,9 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { services } from '../functional/services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); return { // default to the xpack functional config diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/index.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/index.ts index b87f43f73ed2e..9465de1de5922 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/index.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/index.ts @@ -14,8 +14,6 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const searchSessions = getService('searchSessions'); describe('Dashboard', function () { - this.tags('ciGroup5'); - before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await esArchiver.load('x-pack/test/functional/es_archives/dashboard/async_search'); diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/index.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/index.ts index 6a13dc268b705..1ff11eb988456 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/index.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/index.ts @@ -13,8 +13,6 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const PageObjects = getPageObjects(['common']); describe('Search session sharing', function () { - this.tags('ciGroup5'); - before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/index.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/index.ts index 1f8d219674428..2af94730ff918 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/discover/index.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/discover/index.ts @@ -14,8 +14,6 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid const searchSessions = getService('searchSessions'); describe('Discover', function () { - this.tags('ciGroup5'); - before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); diff --git a/x-pack/test/search_sessions_integration/tests/apps/lens/index.ts b/x-pack/test/search_sessions_integration/tests/apps/lens/index.ts index 5e45db7bd7233..978c5bdec0df5 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/lens/index.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/lens/index.ts @@ -12,8 +12,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('lens search sessions', function () { - this.tags('ciGroup5'); - before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); diff --git a/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/index.ts b/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/index.ts index b72d8db06a1d8..60e4ea1b3bfbb 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/index.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/index.ts @@ -12,8 +12,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); describe('search sessions management', function () { - this.tags('ciGroup5'); - before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); await esArchiver.load('x-pack/test/functional/es_archives/dashboard/async_search'); diff --git a/x-pack/test/security_api_integration/tests/anonymous/index.ts b/x-pack/test/security_api_integration/tests/anonymous/index.ts index 08f075950a57f..0f976589483a8 100644 --- a/x-pack/test/security_api_integration/tests/anonymous/index.ts +++ b/x-pack/test/security_api_integration/tests/anonymous/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Anonymous access', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./login')); loadTestFile(require.resolve('./capabilities')); }); diff --git a/x-pack/test/security_api_integration/tests/audit/index.ts b/x-pack/test/security_api_integration/tests/audit/index.ts index 14628bbc51e89..96b2ceb5ae3a7 100644 --- a/x-pack/test/security_api_integration/tests/audit/index.ts +++ b/x-pack/test/security_api_integration/tests/audit/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Audit Log', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./audit_log')); }); } diff --git a/x-pack/test/security_api_integration/tests/http_bearer/index.ts b/x-pack/test/security_api_integration/tests/http_bearer/index.ts index 4dbad2660ebaa..c796c0be9befb 100644 --- a/x-pack/test/security_api_integration/tests/http_bearer/index.ts +++ b/x-pack/test/security_api_integration/tests/http_bearer/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - HTTP Bearer', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./header')); }); } diff --git a/x-pack/test/security_api_integration/tests/http_no_auth_providers/index.ts b/x-pack/test/security_api_integration/tests/http_no_auth_providers/index.ts index 652bcc419e243..23096b2449c9f 100644 --- a/x-pack/test/security_api_integration/tests/http_no_auth_providers/index.ts +++ b/x-pack/test/security_api_integration/tests/http_no_auth_providers/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - HTTP no authentication providers are enabled', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./authentication')); }); } diff --git a/x-pack/test/security_api_integration/tests/kerberos/index.ts b/x-pack/test/security_api_integration/tests/kerberos/index.ts index cec92939a5194..828ce7220458f 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/index.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Kerberos', function () { - this.tags('ciGroup31'); - loadTestFile(require.resolve('./kerberos_login')); }); } diff --git a/x-pack/test/security_api_integration/tests/login_selector/index.ts b/x-pack/test/security_api_integration/tests/login_selector/index.ts index a38a0acc68cca..e3698340d3967 100644 --- a/x-pack/test/security_api_integration/tests/login_selector/index.ts +++ b/x-pack/test/security_api_integration/tests/login_selector/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Login Selector', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./basic_functionality')); }); } diff --git a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/index.ts b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/index.ts index 858b8e2fbb750..2c8edc1569bd2 100644 --- a/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/index.ts +++ b/x-pack/test/security_api_integration/tests/oidc/authorization_code_flow/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - OIDC (Authorization Code Flow)', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./oidc_auth')); }); } diff --git a/x-pack/test/security_api_integration/tests/oidc/implicit_flow/index.ts b/x-pack/test/security_api_integration/tests/oidc/implicit_flow/index.ts index c42014661b915..7479ba8e7bd81 100644 --- a/x-pack/test/security_api_integration/tests/oidc/implicit_flow/index.ts +++ b/x-pack/test/security_api_integration/tests/oidc/implicit_flow/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - OIDC (Implicit Flow)', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./oidc_auth')); }); } diff --git a/x-pack/test/security_api_integration/tests/pki/index.ts b/x-pack/test/security_api_integration/tests/pki/index.ts index c3b733d0b31f8..9926f16619898 100644 --- a/x-pack/test/security_api_integration/tests/pki/index.ts +++ b/x-pack/test/security_api_integration/tests/pki/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - PKI', function () { - this.tags('ciGroup22'); - loadTestFile(require.resolve('./pki_auth')); }); } diff --git a/x-pack/test/security_api_integration/tests/saml/index.ts b/x-pack/test/security_api_integration/tests/saml/index.ts index e7ffdbd410de3..3597f1a6104ec 100644 --- a/x-pack/test/security_api_integration/tests/saml/index.ts +++ b/x-pack/test/security_api_integration/tests/saml/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - SAML', function () { - this.tags('ciGroup27'); - loadTestFile(require.resolve('./saml_login')); }); } diff --git a/x-pack/test/security_api_integration/tests/session_idle/index.ts b/x-pack/test/security_api_integration/tests/session_idle/index.ts index 76457ee7ad0c7..6966b9f2ed2c7 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/index.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Idle', function () { - this.tags('ciGroup18'); - loadTestFile(require.resolve('./cleanup')); loadTestFile(require.resolve('./extension')); }); diff --git a/x-pack/test/security_api_integration/tests/session_invalidate/index.ts b/x-pack/test/security_api_integration/tests/session_invalidate/index.ts index 6408e4cfbd43d..dcfb3d7fc5259 100644 --- a/x-pack/test/security_api_integration/tests/session_invalidate/index.ts +++ b/x-pack/test/security_api_integration/tests/session_invalidate/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Invalidate', function () { - this.tags('ciGroup6'); - loadTestFile(require.resolve('./invalidate')); }); } diff --git a/x-pack/test/security_api_integration/tests/session_lifespan/index.ts b/x-pack/test/security_api_integration/tests/session_lifespan/index.ts index 15522da907958..e297805b4ab3c 100644 --- a/x-pack/test/security_api_integration/tests/session_lifespan/index.ts +++ b/x-pack/test/security_api_integration/tests/session_lifespan/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Lifespan', function () { - this.tags('ciGroup6'); - loadTestFile(require.resolve('./cleanup')); }); } diff --git a/x-pack/test/security_api_integration/tests/token/index.ts b/x-pack/test/security_api_integration/tests/token/index.ts index 88c82125ee1d9..54717dc1c8617 100644 --- a/x-pack/test/security_api_integration/tests/token/index.ts +++ b/x-pack/test/security_api_integration/tests/token/index.ts @@ -9,7 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Token', function () { - this.tags('ciGroup6'); loadTestFile(require.resolve('./login')); loadTestFile(require.resolve('./logout')); loadTestFile(require.resolve('./header')); diff --git a/x-pack/test/security_functional/login_selector.config.ts b/x-pack/test/security_functional/login_selector.config.ts index aa145e2ec6216..d2035a9b228e8 100644 --- a/x-pack/test/security_functional/login_selector.config.ts +++ b/x-pack/test/security_functional/login_selector.config.ts @@ -17,7 +17,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); const kibanaPort = kibanaFunctionalConfig.get('servers.kibana.port'); diff --git a/x-pack/test/security_functional/oidc.config.ts b/x-pack/test/security_functional/oidc.config.ts index 9c00960671e03..6476bbb501b77 100644 --- a/x-pack/test/security_functional/oidc.config.ts +++ b/x-pack/test/security_functional/oidc.config.ts @@ -17,7 +17,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); const kibanaPort = kibanaFunctionalConfig.get('servers.kibana.port'); diff --git a/x-pack/test/security_functional/saml.config.ts b/x-pack/test/security_functional/saml.config.ts index 264197c961123..60a934a712bbf 100644 --- a/x-pack/test/security_functional/saml.config.ts +++ b/x-pack/test/security_functional/saml.config.ts @@ -17,7 +17,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const kibanaFunctionalConfig = await readConfigFile( - require.resolve('../../../test/functional/config.js') + require.resolve('../../../test/functional/config.base.js') ); const kibanaPort = kibanaFunctionalConfig.get('servers.kibana.port'); diff --git a/x-pack/test/security_functional/tests/login_selector/index.ts b/x-pack/test/security_functional/tests/login_selector/index.ts index 1a34fc5eac6d9..bf3cc557f0bd7 100644 --- a/x-pack/test/security_functional/tests/login_selector/index.ts +++ b/x-pack/test/security_functional/tests/login_selector/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - login selector', function () { - this.tags('ciGroup13'); - loadTestFile(require.resolve('./basic_functionality')); loadTestFile(require.resolve('./auth_provider_hint')); }); diff --git a/x-pack/test/security_functional/tests/oidc/index.ts b/x-pack/test/security_functional/tests/oidc/index.ts index cd328384febd3..37490a0193089 100644 --- a/x-pack/test/security_functional/tests/oidc/index.ts +++ b/x-pack/test/security_functional/tests/oidc/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - OIDC interactions', function () { - this.tags('ciGroup13'); - loadTestFile(require.resolve('./url_capture')); }); } diff --git a/x-pack/test/security_functional/tests/saml/index.ts b/x-pack/test/security_functional/tests/saml/index.ts index 66a497db8af40..ebf97ebf8edfb 100644 --- a/x-pack/test/security_functional/tests/saml/index.ts +++ b/x-pack/test/security_functional/tests/saml/index.ts @@ -9,8 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app - SAML interactions', function () { - this.tags('ciGroup13'); - loadTestFile(require.resolve('./url_capture')); }); } diff --git a/x-pack/test/security_solution_cypress/config.firefox.ts b/x-pack/test/security_solution_cypress/config.firefox.ts index 2a2ce410850ff..c29f47708a170 100644 --- a/x-pack/test/security_solution_cypress/config.firefox.ts +++ b/x-pack/test/security_solution_cypress/config.firefox.ts @@ -16,7 +16,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') + require.resolve('../functional/config.base.js') ); return { diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 7f196880e8fca..4b5b2c361c1b9 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -14,7 +14,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('../../../test/common/config.js') ); const xpackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') + require.resolve('../functional/config.base.js') ); return { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index 48dc450737229..0168a15f21b6e 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -82,7 +82,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await deleteAllDocsFromMetadataUnitedIndex(getService); await pageObjects.endpoint.navigateToEndpointList(); }); - it('finds no data in list and prompts onboarding to add policy', async () => { await testSubjects.exists('emptyPolicyTable'); }); @@ -99,7 +98,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { after(async () => { await deleteAllDocsFromMetadataCurrentIndex(getService); await deleteAllDocsFromMetadataUnitedIndex(getService); - await endpointTestResources.unloadEndpointData(indexedData); + if (indexedData) { + await endpointTestResources.unloadEndpointData(indexedData); + } }); it('finds page title', async () => { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts index e57895f4f32b7..f74bd3b91cfce 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts @@ -16,7 +16,6 @@ export default function (providerContext: FtrProviderContext) { const { loadTestFile, getService } = providerContext; describe('endpoint', function () { - this.tags('ciGroup7'); const ingestManager = getService('ingestManager'); const log = getService('log'); const endpointTestResources = getService('endpointTestResources'); @@ -36,6 +35,7 @@ export default function (providerContext: FtrProviderContext) { await endpointTestResources.installOrUpgradeEndpointFleetPackage(); }); loadTestFile(require.resolve('./endpoint_list')); + loadTestFile(require.resolve('./policy_list')); loadTestFile(require.resolve('./policy_details')); loadTestFile(require.resolve('./endpoint_telemetry')); loadTestFile(require.resolve('./trusted_apps_list')); 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 9bf6a19b4a589..c95afc743c839 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 @@ -331,6 +331,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should show event filters card and link should go back to policy', async () => { await testSubjects.existOrFail('eventFilters-fleet-integration-card'); + const eventFiltersCard = await testSubjects.find('eventFilters-fleet-integration-card'); + await pageObjects.ingestManagerCreatePackagePolicy.scrollToCenterOfWindow(eventFiltersCard); await (await testSubjects.find('eventFilters-link-to-exceptions')).click(); await testSubjects.existOrFail('policyDetailsPage'); await (await testSubjects.find('policyDetailsBackLink')).click(); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts new file mode 100644 index 0000000000000..840c36a558ba0 --- /dev/null +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { PolicyTestResourceInfo } from '../../services/endpoint_policy'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const browser = getService('browser'); + const pageObjects = getPageObjects([ + 'common', + 'endpoint', + 'policy', + 'endpointPageUtils', + 'ingestManagerCreatePackagePolicy', + 'trustedApps', + ]); + const testSubjects = getService('testSubjects'); + const policyTestResources = getService('policyTestResources'); + const endpointTestResources = getService('endpointTestResources'); + + describe('When on the Endpoint Policy List Page', () => { + before(async () => { + const endpointPackage = await policyTestResources.getEndpointPackage(); + await endpointTestResources.setMetadataTransformFrequency('1s', endpointPackage.version); + await browser.refresh(); + }); + + describe('with no policies', () => { + it('shows the empty page', async () => { + await pageObjects.policy.navigateToPolicyList(); + await testSubjects.existOrFail('emptyPolicyTable'); + }); + it('navigates to Fleet and ensures the integration page is loaded correctly', async () => { + const fleetButton = await testSubjects.find('onboardingStartButton'); + await fleetButton.click(); + await testSubjects.existOrFail('createPackagePolicy_pageTitle'); + expect(await testSubjects.getVisibleText('createPackagePolicy_pageTitle')).to.equal( + 'Add Endpoint Security integration' + ); + }); + it('navigates back to the policy list page', async () => { + const cancelButton = await testSubjects.find('createPackagePolicy_cancelBackLink'); + cancelButton.click(); + await pageObjects.policy.ensureIsOnListPage(); + }); + }); + describe('with policies', () => { + let indexedData: IndexedHostsAndAlertsResponse; + let policyInfo: PolicyTestResourceInfo; + before(async () => { + indexedData = await endpointTestResources.loadEndpointData(); + policyInfo = await policyTestResources.createPolicy(); + await browser.refresh(); + }); + after(async () => { + if (indexedData) { + await endpointTestResources.unloadEndpointData(indexedData); + } + if (policyInfo) { + await policyInfo.cleanup(); + } + }); + it('shows the policy list table', async () => { + await pageObjects.policy.navigateToPolicyList(); + await testSubjects.existOrFail('policyListTable'); + }); + it('navigates to the policy details page when the policy name is clicked and returns back to the policy list page using the header back button', async () => { + const policyName = (await testSubjects.findAll('policyNameCellLink'))[0]; + await policyName.click(); + await pageObjects.policy.ensureIsOnDetailsPage(); + const backButton = await testSubjects.find('policyDetailsBackLink'); + await backButton.click(); + await pageObjects.policy.ensureIsOnListPage(); + }); + // FLAKY: https://github.com/elastic/kibana/issues/131602 + describe.skip('when the endpoint count link is clicked', () => { + it('navigates to the endpoint list page filtered by policy', async () => { + const endpointCount = (await testSubjects.findAll('policyEndpointCountLink'))[0]; + await endpointCount.click(); + await pageObjects.endpoint.ensureIsOnEndpointListPage(); + }); + it('admin searchbar contains the selected policy id', async () => { + const expectedPolicyId = indexedData.integrationPolicies[0].id; + await pageObjects.endpoint.ensureIsOnEndpointListPage(); + expect(await testSubjects.getVisibleText('adminSearchBar')).to.equal( + `united.endpoint.Endpoint.policy.applied.id : "${expectedPolicyId}"` + ); + }); + it('endpoint table shows the endpoints associated with selected policy', async () => { + const expectedPolicyName = indexedData.integrationPolicies[0].name; + await pageObjects.endpoint.ensureIsOnEndpointListPage(); + const policyName = (await testSubjects.findAll('policyNameCellLink'))[0]; + expect(await policyName.getVisibleText()).to.be.equal( + expectedPolicyName.substring(0, expectedPolicyName.indexOf('-')) + ); + }); + it('returns back to the policy list page when the header back button is clicked', async () => { + await pageObjects.endpoint.ensureIsOnEndpointListPage(); + const backButton = await testSubjects.find('endpointListBackLink'); + await backButton.click(); + await pageObjects.policy.ensureIsOnListPage(); + }); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint/config.ts b/x-pack/test/security_solution_endpoint/config.ts index b00df7732ea4f..b5b52b7bc5cd5 100644 --- a/x-pack/test/security_solution_endpoint/config.ts +++ b/x-pack/test/security_solution_endpoint/config.ts @@ -15,7 +15,9 @@ import { } from '../security_solution_endpoint_api_int/registry'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); return { ...xpackFunctionalConfig.getAll(), diff --git a/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts b/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts index d4e2479d24bc7..21d7b25cabb34 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts @@ -25,6 +25,10 @@ export function EndpointPageProvider({ getService, getPageObjects }: FtrProvider await pageObjects.header.waitUntilLoadingHasFinished(); }, + async ensureIsOnEndpointListPage() { + await testSubjects.existOrFail('endpointPage'); + }, + async waitForTableToHaveData(dataTestSubj: string, timeout = 2000) { await retry.waitForWithTimeout('table to have data', timeout, async () => { const tableData = await pageObjects.endpointPageUtils.tableData(dataTestSubj); diff --git a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts index b5eccd0ef1147..da4fb936d6655 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts @@ -14,6 +14,16 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr const retryService = getService('retry'); return { + /** + * Navigates to the Endpoint Policy List page + */ + async navigateToPolicyList() { + await pageObjects.common.navigateToUrlWithBrowserHistory( + 'securitySolutionManagement', + `/policy` + ); + await pageObjects.header.waitUntilLoadingHasFinished(); + }, /** * Navigates to the Endpoint Policy Details page * @@ -27,6 +37,12 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr await pageObjects.header.waitUntilLoadingHasFinished(); }, + /** + * Ensures the current page is the policy list page + */ + async ensureIsOnListPage() { + await testSubjects.existOrFail('policyListPage'); + }, /** * Finds and returns the Policy Details Page Save button */ @@ -127,7 +143,7 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr /** * Used when looking a the Ingest create/edit package policy pages. Finds the endpoint - * custom configuaration component + * custom configuration component * @param onEditPage */ async findPackagePolicyEndpointCustomConfiguration(onEditPage: boolean = false) { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index 3c98b703aed55..7be4ce2243303 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -13,8 +13,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider const { loadTestFile, getService } = providerContext; describe('Endpoint plugin', function () { - this.tags('ciGroup9'); - const ingestManager = getService('ingestManager'); const log = getService('log'); diff --git a/x-pack/test/spaces_api_integration/common/config.ts b/x-pack/test/spaces_api_integration/common/config.ts index 5d135cd05605c..15a63fec6d309 100644 --- a/x-pack/test/spaces_api_integration/common/config.ts +++ b/x-pack/test/spaces_api_integration/common/config.ts @@ -21,7 +21,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) const config = { kibana: { api: await readConfigFile(path.resolve(REPO_ROOT, 'test/api_integration/config.js')), - functional: await readConfigFile(require.resolve('../../../../test/functional/config.js')), + functional: await readConfigFile( + require.resolve('../../../../test/functional/config.base.js') + ), }, xpack: { api: await readConfigFile(require.resolve('../../api_integration/config.ts')), diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts index a86fef0d758fc..75381f35dacd3 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts @@ -14,8 +14,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { const supertest = getService('supertest'); describe('spaces api with security', function () { - this.tags('ciGroup8'); - before(async () => { await createUsersAndRoles(es, supertest); }); diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts index f64336b2b4908..6a8148efaa1d6 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts @@ -10,8 +10,6 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function spacesOnlyTestSuite({ loadTestFile }: FtrProviderContext) { describe('spaces api without security', function () { - this.tags('ciGroup5'); - loadTestFile(require.resolve('./copy_to_space')); loadTestFile(require.resolve('./resolve_copy_to_space_conflicts')); loadTestFile(require.resolve('./create')); diff --git a/x-pack/test/stack_functional_integration/apps/telemetry/index.js b/x-pack/test/stack_functional_integration/apps/telemetry/index.js index 80cffcfaf70a7..3d1dffb3a442f 100644 --- a/x-pack/test/stack_functional_integration/apps/telemetry/index.js +++ b/x-pack/test/stack_functional_integration/apps/telemetry/index.js @@ -7,7 +7,6 @@ export default function ({ loadTestFile }) { describe('telemetry feature', function () { - this.tags('ciGroup1'); loadTestFile(require.resolve('./_telemetry')); }); } diff --git a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js index 9b768ab61baec..1658bcbf6cd35 100644 --- a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js +++ b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js @@ -25,7 +25,9 @@ const testsFolder = '../apps'; const prepend = (testFile) => require.resolve(`${testsFolder}/${testFile}`); export default async ({ readConfigFile }) => { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../../functional/config')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../../functional/config.base.js') + ); const externalConf = consumeState(resolve(__dirname, stateFilePath)); process.env.stack_functional_integration = true; logAll(log); diff --git a/x-pack/test/timeline/security_and_spaces/tests/basic/index.ts b/x-pack/test/timeline/security_and_spaces/tests/basic/index.ts index 4672a8e2e7f65..248c5ece3641d 100644 --- a/x-pack/test/timeline/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/timeline/security_and_spaces/tests/basic/index.ts @@ -14,9 +14,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('timeline security and spaces enabled: basic', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - before(async () => { await createSpacesAndUsers(getService); }); diff --git a/x-pack/test/timeline/security_and_spaces/tests/trial/index.ts b/x-pack/test/timeline/security_and_spaces/tests/trial/index.ts index 736fb6619c82d..2103097891a31 100644 --- a/x-pack/test/timeline/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/timeline/security_and_spaces/tests/trial/index.ts @@ -38,9 +38,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('timeline security and spaces enabled: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup5'); - before(async () => { await createSpaces(getService); await createUsersAndRoles( diff --git a/x-pack/test/ui_capabilities/common/config.ts b/x-pack/test/ui_capabilities/common/config.ts index f676a5eeccee1..32e7538ecbbe7 100644 --- a/x-pack/test/ui_capabilities/common/config.ts +++ b/x-pack/test/ui_capabilities/common/config.ts @@ -20,7 +20,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) return async ({ readConfigFile }: FtrConfigProviderContext) => { const xPackFunctionalTestsConfig = await readConfigFile( - require.resolve('../../functional/config.js') + require.resolve('../../functional/config.base.js') ); return { diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts index a257f8fcabb8e..77619157c615f 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts @@ -16,8 +16,6 @@ export default function uiCapabilitiesTests({ loadTestFile, getService }: FtrPro const featuresService: FeaturesService = getService('features'); describe('ui capabilities', function () { - this.tags('ciGroup9'); - before(async () => { const features = await featuresService.get(); for (const space of Spaces) { diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/index.ts b/x-pack/test/ui_capabilities/spaces_only/tests/index.ts index 73a068cf9ec6b..02ed1533b56ad 100644 --- a/x-pack/test/ui_capabilities/spaces_only/tests/index.ts +++ b/x-pack/test/ui_capabilities/spaces_only/tests/index.ts @@ -14,8 +14,6 @@ export default function uiCapabilitesTests({ loadTestFile, getService }: FtrProv const featuresService: FeaturesService = getService('features'); describe('ui capabilities', function () { - this.tags('ciGroup9'); - before(async () => { // we're using a basic license, so if we want to disable all features, we have to ignore the valid licenses const features = await featuresService.get({ ignoreValidLicenses: true }); diff --git a/x-pack/test/upgrade/apps/discover/discover_smoke_tests.ts b/x-pack/test/upgrade/apps/discover/discover_smoke_tests.ts index 150458919d41d..1d2df7a703161 100644 --- a/x-pack/test/upgrade/apps/discover/discover_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/discover/discover_smoke_tests.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getPageObjects, getService }: FtrProviderContext) { +export default function ({ getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'home', 'discover', 'timePicker']); describe('upgrade discover smoke tests', function describeIndexTests() { @@ -18,9 +18,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ]; const discoverTests = [ - { name: 'kibana_sample_data_flights', timefield: true, hits: '' }, - { name: 'kibana_sample_data_logs', timefield: true, hits: '' }, - { name: 'kibana_sample_data_ecommerce', timefield: true, hits: '' }, + { name: 'flights', timefield: true, hits: '' }, + { name: 'logs', timefield: true, hits: '' }, + { name: 'ecommerce', timefield: true, hits: '' }, ]; spaces.forEach(({ space, basePath }) => { @@ -31,7 +31,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { basePath, }); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.discover.selectIndexPattern(name); + await PageObjects.discover.selectIndexPattern(`kibana_sample_data_${name}`); await PageObjects.discover.waitUntilSearchingHasFinished(); if (timefield) { await PageObjects.timePicker.setCommonlyUsedTime('Last_24 hours'); @@ -52,6 +52,35 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); }); + + discoverTests.forEach(({ name, timefield, hits }) => { + describe('space: ' + space + ', name: ' + name, () => { + before(async () => { + await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', { + basePath, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.launchSampleDiscover(name); + await PageObjects.header.waitUntilLoadingHasFinished(); + if (timefield) { + await PageObjects.timePicker.setCommonlyUsedTime('Last_24 hours'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + } + }); + it('shows hit count greater than zero', async () => { + const hitCount = await PageObjects.discover.getHitCount(); + if (hits === '') { + expect(hitCount).to.be.greaterThan(0); + } else { + expect(hitCount).to.be.equal(hits); + } + }); + it('shows table rows not empty', async () => { + const tableRows = await PageObjects.discover.getDocTableRows(); + expect(tableRows.length).to.be.greaterThan(0); + }); + }); + }); }); }); } diff --git a/x-pack/test/upgrade/config.ts b/x-pack/test/upgrade/config.ts index 78d61d5239556..181abe8ca408f 100644 --- a/x-pack/test/upgrade/config.ts +++ b/x-pack/test/upgrade/config.ts @@ -12,7 +12,7 @@ import { MapsHelper } from './services/maps_upgrade_services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const apiConfig = await readConfigFile(require.resolve('../api_integration/config')); - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { ...functionalConfig.getAll(), diff --git a/x-pack/test/upgrade_assistant_integration/config.js b/x-pack/test/upgrade_assistant_integration/config.js index 8152b5790fc40..e1248c717b216 100644 --- a/x-pack/test/upgrade_assistant_integration/config.js +++ b/x-pack/test/upgrade_assistant_integration/config.js @@ -11,7 +11,7 @@ export default async function ({ readConfigFile }) { require.resolve('../../../test/api_integration/config.js') ); const xPackFunctionalTestsConfig = await readConfigFile( - require.resolve('../functional/config.js') + require.resolve('../functional/config.base.js') ); const kibanaCommonConfig = await readConfigFile( require.resolve('../../../test/common/config.js') diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js index 1a7090a3cbdfb..eb09d24b79b6a 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js @@ -7,8 +7,6 @@ export default function ({ loadTestFile }) { describe('upgrade assistant', function () { - this.tags('ciGroup7'); - loadTestFile(require.resolve('./reindexing')); }); } diff --git a/x-pack/test/usage_collection/config.ts b/x-pack/test/usage_collection/config.ts index beb934219422a..248f00ff81233 100644 --- a/x-pack/test/usage_collection/config.ts +++ b/x-pack/test/usage_collection/config.ts @@ -15,7 +15,9 @@ import { pageObjects } from './page_objects'; // that returns an object with the projects config values export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + const xpackFunctionalConfig = await readConfigFile( + require.resolve('../functional/config.base.js') + ); // Find all folders in ./plugins since we treat all them as plugin folder const allFiles = fs.readdirSync(resolve(__dirname, 'plugins')); diff --git a/x-pack/test/usage_collection/test_suites/application_usage/index.ts b/x-pack/test/usage_collection/test_suites/application_usage/index.ts index 4b41aada9ad29..754ae98997c16 100644 --- a/x-pack/test/usage_collection/test_suites/application_usage/index.ts +++ b/x-pack/test/usage_collection/test_suites/application_usage/index.ts @@ -11,7 +11,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Application Usage', function () { - this.tags('ciGroup1'); const { common } = getPageObjects(['common']); const browser = getService('browser'); diff --git a/x-pack/test/usage_collection/test_suites/stack_management_usage/index.ts b/x-pack/test/usage_collection/test_suites/stack_management_usage/index.ts index dac552220f7c1..a595c2662d451 100644 --- a/x-pack/test/usage_collection/test_suites/stack_management_usage/index.ts +++ b/x-pack/test/usage_collection/test_suites/stack_management_usage/index.ts @@ -12,7 +12,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/119038 describe.skip('Stack Management', function () { - this.tags('ciGroup1'); const { common } = getPageObjects(['common']); const browser = getService('browser'); diff --git a/x-pack/test/visual_regression/config.ts b/x-pack/test/visual_regression/config.ts index c211918ef8e52..c7f0d8203833e 100644 --- a/x-pack/test/visual_regression/config.ts +++ b/x-pack/test/visual_regression/config.ts @@ -10,7 +10,7 @@ import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../functional/config.base.js')); return { ...functionalConfig.getAll(), diff --git a/x-pack/test/visual_regression/tests/canvas/index.js b/x-pack/test/visual_regression/tests/canvas/index.js index 099c96e6eaf01..20a262fef10fe 100644 --- a/x-pack/test/visual_regression/tests/canvas/index.js +++ b/x-pack/test/visual_regression/tests/canvas/index.js @@ -25,7 +25,6 @@ export default function ({ loadTestFile, getService }) { await esArchiver.unload('x-pack/test/functional/es_archives/canvas/default'); }); - this.tags('ciGroup10'); loadTestFile(require.resolve('./fullscreen')); }); } diff --git a/x-pack/test/visual_regression/tests/infra/index.js b/x-pack/test/visual_regression/tests/infra/index.js index b624c6ec848f2..13669c50953f9 100644 --- a/x-pack/test/visual_regression/tests/infra/index.js +++ b/x-pack/test/visual_regression/tests/infra/index.js @@ -13,7 +13,6 @@ export default function ({ loadTestFile, getService }) { await browser.setWindowSize(1600, 1000); }); - this.tags('ciGroup10'); loadTestFile(require.resolve('./waffle_map')); loadTestFile(require.resolve('./saved_views')); }); diff --git a/x-pack/test/visual_regression/tests/maps/index.js b/x-pack/test/visual_regression/tests/maps/index.js index 3459896baacd6..9d53d70ad2abc 100644 --- a/x-pack/test/visual_regression/tests/maps/index.js +++ b/x-pack/test/visual_regression/tests/maps/index.js @@ -56,7 +56,6 @@ export default function ({ loadTestFile, getService }) { ); }); - this.tags('ciGroup10'); loadTestFile(require.resolve('./vector_styling')); }); } diff --git a/yarn.lock b/yarn.lock index 426588b11a9c4..439a288a9db59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2932,6 +2932,18 @@ version "0.0.0" uid "" +"@kbn/analytics-shippers-elastic-v3-browser@link:bazel-bin/packages/analytics/shippers/elastic_v3/browser": + version "0.0.0" + uid "" + +"@kbn/analytics-shippers-elastic-v3-common@link:bazel-bin/packages/analytics/shippers/elastic_v3/common": + version "0.0.0" + uid "" + +"@kbn/analytics-shippers-elastic-v3-server@link:bazel-bin/packages/analytics/shippers/elastic_v3/server": + version "0.0.0" + uid "" + "@kbn/analytics-shippers-fullstory@link:bazel-bin/packages/analytics/shippers/fullstory": version "0.0.0" uid "" @@ -3100,6 +3112,10 @@ version "0.0.0" uid "" +"@kbn/performance-testing-dataset-extractor@link:bazel-bin/packages/kbn-performance-testing-dataset-extractor": + version "0.0.0" + uid "" + "@kbn/plugin-discovery@link:bazel-bin/packages/kbn-plugin-discovery": version "0.0.0" uid "" @@ -6032,6 +6048,18 @@ version "0.0.0" uid "" +"@types/kbn__analytics-shippers-elastic-v3-browser@link:bazel-bin/packages/analytics/shippers/elastic_v3/browser/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__analytics-shippers-elastic-v3-common@link:bazel-bin/packages/analytics/shippers/elastic_v3/common/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__analytics-shippers-elastic-v3-server@link:bazel-bin/packages/analytics/shippers/elastic_v3/server/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__analytics-shippers-fullstory@link:bazel-bin/packages/analytics/shippers/fullstory/npm_module_types": version "0.0.0" uid "" @@ -6176,6 +6204,10 @@ version "0.0.0" uid "" +"@types/kbn__performance-testing-dataset-extractor@link:bazel-bin/packages/kbn-performance-testing-dataset-extractor/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__plugin-discovery@link:bazel-bin/packages/kbn-plugin-discovery/npm_module_types": version "0.0.0" uid "" @@ -13009,10 +13041,10 @@ elastic-apm-http-client@11.0.1: semver "^6.3.0" stream-chopper "^3.0.1" -elastic-apm-node@^3.32.0: - version "3.32.0" - resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.32.0.tgz#fdad3c03aabc4e7994b4155b031f68d9774af49a" - integrity sha512-6vOe1FZv5toCouuyfiXZuWNE1+1fim9zvsv7H56BKRYa7xQ3X1fxq7QAP2gLd/Z9zvSDLGNXS4DPE1eqX1A1Jw== +elastic-apm-node@^3.33.0: + version "3.33.0" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.33.0.tgz#ad2850b005355299c3a9fdc631875162480ceb15" + integrity sha512-ZJKcRbYdEU87MWyB9CczGTLEERA595OWd0nqpxrDBkogogpxwpzCOialKZA+bekpNA0Oa4Sv18zRCdyqpV25Pw== dependencies: "@elastic/ecs-pino-format" "^1.2.0" after-all-results "^2.0.0" @@ -13044,7 +13076,6 @@ elastic-apm-node@^3.32.0: shallow-clone-shim "^2.0.0" source-map "^0.8.0-beta.0" sql-summary "^1.0.1" - traceparent "^1.0.0" traverse "^0.6.6" unicode-byte-truncate "^1.0.0" @@ -23577,11 +23608,6 @@ random-bytes@~1.0.0: resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= -random-poly-fill@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/random-poly-fill/-/random-poly-fill-1.0.1.tgz#13634dc0255a31ecf85d4a182d92c40f9bbcf5ed" - integrity sha512-bMOL0hLfrNs52+EHtIPIXxn2PxYwXb0qjnKruTjXiM/sKfYqj506aB2plFwWW1HN+ri724bAVVGparh4AtlJKw== - random-word-slugs@^0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/random-word-slugs/-/random-word-slugs-0.0.5.tgz#6ccd6c7ea320be9fbc19507f8c3a7d4a970ff61f" @@ -28087,13 +28113,6 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= -traceparent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/traceparent/-/traceparent-1.0.0.tgz#9b14445cdfe5c19f023f1c04d249c3d8e003a5ce" - integrity sha512-b/hAbgx57pANQ6cg2eBguY3oxD6FGVLI1CC2qoi01RmHR7AYpQHPXTig9FkzbWohEsVuHENZHP09aXuw3/LM+w== - dependencies: - random-poly-fill "^1.0.1" - traverse@^0.6.6, traverse@~0.6.6: version "0.6.6" resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"